Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
namespace JdeScoping.DataAccess.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Excel output column specification attribute.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class OutputColumnAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard format.
|
||||
/// </summary>
|
||||
public const string StdFormat = "@";
|
||||
|
||||
/// <summary>
|
||||
/// Standard date format.
|
||||
/// </summary>
|
||||
public const string DateFormat = "[$-409]MM/dd/yyyy;@";
|
||||
|
||||
/// <summary>
|
||||
/// Standard timestamp format.
|
||||
/// </summary>
|
||||
public const string TimestampFormat = "[$-409]m/d/yy h:mm AM/PM;@";
|
||||
|
||||
/// <summary>
|
||||
/// Wrapped text column default width.
|
||||
/// </summary>
|
||||
public const double WrappedColumnWidth = 65;
|
||||
|
||||
/// <summary>
|
||||
/// Order to display column.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override text to display for column header.
|
||||
/// </summary>
|
||||
public string HeaderText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Column format (Excel formatting string).
|
||||
/// </summary>
|
||||
public string Format { get; set; } = StdFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not width should be set automatically.
|
||||
/// </summary>
|
||||
public bool AutoWidth { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Manually set width (only used if AutoWidth = FALSE).
|
||||
/// </summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not text should be wrapped.
|
||||
/// </summary>
|
||||
public bool WrapText { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace JdeScoping.DataAccess.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Excel output table specification attribute.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
public class OutputTableAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Output tab name in Excel.
|
||||
/// </summary>
|
||||
public string TabName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Table name for the Excel table.
|
||||
/// </summary>
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not merged header should be shown.
|
||||
/// </summary>
|
||||
public bool ShowHeader { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace JdeScoping.DataAccess.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the data access layer.
|
||||
/// </summary>
|
||||
public class DataAccessOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name for binding.
|
||||
/// </summary>
|
||||
public const string SectionName = "DataAccess";
|
||||
|
||||
/// <summary>
|
||||
/// Default timeout for database queries in seconds.
|
||||
/// </summary>
|
||||
public int DefaultTimeoutSeconds { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for lot usage queries in seconds (very large dataset).
|
||||
/// </summary>
|
||||
public int LotUsageTimeoutSeconds { get; set; } = 999999;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for MIS data queries in seconds.
|
||||
/// </summary>
|
||||
public int MisDataTimeoutSeconds { get; set; } = 60000;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for index rebuild operations in seconds.
|
||||
/// </summary>
|
||||
public int RebuildIndexTimeoutSeconds { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// JDE production schema name (e.g., PRODDTA).
|
||||
/// </summary>
|
||||
public string ProductionSchema { get; set; } = "PRODDTA";
|
||||
|
||||
/// <summary>
|
||||
/// JDE archive schema name (e.g., ARCDTAPD).
|
||||
/// </summary>
|
||||
public string ArchiveSchema { get; set; } = "ARCDTAPD";
|
||||
|
||||
/// <summary>
|
||||
/// JDE stage schema name (e.g., JDESTAGE).
|
||||
/// </summary>
|
||||
public string StageSchema { get; set; } = "JDESTAGE";
|
||||
|
||||
/// <summary>
|
||||
/// Enable detailed SQL logging for debugging.
|
||||
/// </summary>
|
||||
public bool EnableDetailedLogging { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace JdeScoping.DataAccess.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for search processing.
|
||||
/// </summary>
|
||||
public class SearchProcessingConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name in appsettings.json.
|
||||
/// </summary>
|
||||
public const string SectionName = "SearchProcessing";
|
||||
|
||||
/// <summary>
|
||||
/// Query timeout in seconds for search execution.
|
||||
/// </summary>
|
||||
public int QueryTimeoutSeconds { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum downstream traversal iterations.
|
||||
/// </summary>
|
||||
public int MaxTraversalIterations { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Enable debug SQL logging.
|
||||
/// </summary>
|
||||
public bool EnableDebugSql { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Path to write debug SQL files (when EnableDebugSql is true).
|
||||
/// </summary>
|
||||
public string? DebugSqlPath { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using JdeScoping.DataAccess.Exceptions;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.DataAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating database connections to all data sources.
|
||||
/// </summary>
|
||||
public class DbConnectionFactory : IDbConnectionFactory
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<DbConnectionFactory> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DbConnectionFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Application configuration.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public DbConnectionFactory(IConfiguration configuration, ILogger<DbConnectionFactory> logger)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string dataSource = "LotFinderDB";
|
||||
var connectionString = _configuration.GetConnectionString(dataSource);
|
||||
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
throw new ConnectionException(
|
||||
$"{dataSource}: Connection string not found in configuration.",
|
||||
dataSource);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Creating connection to {DataSource}", dataSource);
|
||||
var connection = new SqlConnection(connectionString);
|
||||
await connection.OpenAsync(ct).ConfigureAwait(false);
|
||||
_logger.LogDebug("Successfully connected to {DataSource}", dataSource);
|
||||
return connection;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["DataSource"] = dataSource,
|
||||
["Operation"] = "CreateConnection"
|
||||
}))
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect to {DataSource}", dataSource);
|
||||
}
|
||||
|
||||
throw new ConnectionException(
|
||||
$"{dataSource}: Failed to open connection to database.",
|
||||
dataSource,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OracleConnection> CreateJdeConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await CreateOracleConnectionAsync("JDE", ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OracleConnection> CreateJdeStageConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await CreateOracleConnectionAsync("JDEStage", ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OracleConnection> CreateCmsConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await CreateOracleConnectionAsync("CMS", ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<OracleConnection> CreateOracleConnectionAsync(string dataSource, CancellationToken ct)
|
||||
{
|
||||
var connectionString = _configuration.GetConnectionString(dataSource);
|
||||
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
throw new ConnectionException(
|
||||
$"{dataSource}: Connection string not found in configuration.",
|
||||
dataSource);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Creating connection to {DataSource}", dataSource);
|
||||
var connection = new OracleConnection(connectionString);
|
||||
await connection.OpenAsync(ct).ConfigureAwait(false);
|
||||
_logger.LogDebug("Successfully connected to {DataSource}", dataSource);
|
||||
return connection;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["DataSource"] = dataSource,
|
||||
["Operation"] = "CreateConnection"
|
||||
}))
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect to {DataSource}", dataSource);
|
||||
}
|
||||
|
||||
throw new ConnectionException(
|
||||
$"{dataSource}: Failed to open connection to database.",
|
||||
dataSource,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Options;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Configuration;
|
||||
using JdeScoping.DataAccess.FilterHandlers;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.QueryBuilders;
|
||||
using JdeScoping.DataAccess.Repositories;
|
||||
using JdeScoping.DataAccess.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering data access services.
|
||||
/// </summary>
|
||||
public static class DataAccessDependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds data access layer services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration instance.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddDataAccess(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Register configuration options
|
||||
services.Configure<DataAccessOptions>(
|
||||
configuration.GetSection(DataAccessOptions.SectionName));
|
||||
services.Configure<SearchProcessingOptions>(
|
||||
configuration.GetSection(SearchProcessingOptions.SectionName));
|
||||
services.Configure<SearchProcessingConfiguration>(
|
||||
configuration.GetSection(SearchProcessingConfiguration.SectionName));
|
||||
|
||||
// Register connection factory as singleton
|
||||
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
|
||||
|
||||
// Register repositories as scoped (per-request lifetime)
|
||||
services.AddScoped<ILotFinderRepository, LotFinderRepository>();
|
||||
services.AddScoped<IJdeRepository, JdeRepository>();
|
||||
services.AddScoped<ICmsRepository, CmsRepository>();
|
||||
|
||||
// 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)
|
||||
services.AddScoped<ISearchQueryBuilder, SqlKataSearchQueryBuilder>();
|
||||
|
||||
// Register search processing services (scoped)
|
||||
services.AddScoped<IWorkOrderTraversalService, WorkOrderTraversalService>();
|
||||
services.AddScoped<SearchProcessor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds data access layer services with custom options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">Action to configure options.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddDataAccess(
|
||||
this IServiceCollection services,
|
||||
Action<DataAccessOptions> configureOptions)
|
||||
{
|
||||
// Register configuration options via action
|
||||
services.Configure(configureOptions);
|
||||
|
||||
// Register connection factory as singleton
|
||||
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
|
||||
|
||||
// Register repositories as scoped (per-request lifetime)
|
||||
services.AddScoped<ILotFinderRepository, LotFinderRepository>();
|
||||
services.AddScoped<IJdeRepository, JdeRepository>();
|
||||
services.AddScoped<ICmsRepository, CmsRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace JdeScoping.DataAccess.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a database connection fails.
|
||||
/// </summary>
|
||||
public class ConnectionException : DataAccessException
|
||||
{
|
||||
/// <summary>
|
||||
/// The data source identifier (e.g., "JDE", "CMS", "LotFinderDB").
|
||||
/// </summary>
|
||||
public string? DataSource { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="dataSource">The data source that failed to connect.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public ConnectionException(string message, string? dataSource, Exception? inner = null)
|
||||
: base(message, operation: "CreateConnection", repository: dataSource, inner: inner)
|
||||
{
|
||||
DataSource = dataSource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace JdeScoping.DataAccess.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Base exception for all data access layer errors.
|
||||
/// </summary>
|
||||
public class DataAccessException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation that was being performed when the exception occurred.
|
||||
/// </summary>
|
||||
public string? Operation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository where the exception occurred.
|
||||
/// </summary>
|
||||
public string? Repository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataAccessException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="operation">The operation that was being performed.</param>
|
||||
/// <param name="repository">The repository where the error occurred.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public DataAccessException(
|
||||
string message,
|
||||
string? operation = null,
|
||||
string? repository = null,
|
||||
Exception? inner = null)
|
||||
: base(message, inner)
|
||||
{
|
||||
Operation = operation;
|
||||
Repository = repository;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace JdeScoping.DataAccess.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a database operation times out.
|
||||
/// </summary>
|
||||
public class DataAccessTimeoutException : DataAccessException
|
||||
{
|
||||
/// <summary>
|
||||
/// The configured timeout value in seconds.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataAccessTimeoutException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="timeoutSeconds">The configured timeout value in seconds.</param>
|
||||
/// <param name="operation">The operation that was being performed.</param>
|
||||
/// <param name="repository">The repository where the error occurred.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public DataAccessTimeoutException(
|
||||
string message,
|
||||
int timeoutSeconds,
|
||||
string? operation = null,
|
||||
string? repository = null,
|
||||
Exception? inner = null)
|
||||
: base(message, operation, repository, inner)
|
||||
{
|
||||
TimeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace JdeScoping.DataAccess.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a database query fails.
|
||||
/// </summary>
|
||||
public class QueryException : DataAccessException
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the query that failed.
|
||||
/// </summary>
|
||||
public string? QueryName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="queryName">The name of the query that failed.</param>
|
||||
/// <param name="operation">The operation that was being performed.</param>
|
||||
/// <param name="repository">The repository where the error occurred.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public QueryException(
|
||||
string message,
|
||||
string? queryName,
|
||||
string? operation = null,
|
||||
string? repository = null,
|
||||
Exception? inner = null)
|
||||
: base(message, operation, repository, inner)
|
||||
{
|
||||
QueryName = queryName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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>());
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Quality;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for accessing CMS Oracle database.
|
||||
/// </summary>
|
||||
public interface ICmsRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets Manufacturing Information System (MIS) data from CMS database.
|
||||
/// Uses MisDataTimeoutSeconds timeout due to complex 10-table JOIN.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming MIS data records.</returns>
|
||||
IAsyncEnumerable<MisData> GetMisDataAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating database connections to all data sources.
|
||||
/// </summary>
|
||||
public interface IDbConnectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and opens a connection to the LotFinderDB SQL Server cache database.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>An open SQL Server connection. Caller is responsible for disposal.</returns>
|
||||
Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens a connection to the JDE Oracle database (production schema).
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>An open Oracle connection. Caller is responsible for disposal.</returns>
|
||||
Task<OracleConnection> CreateJdeConnectionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens a connection to the JDE Stage Oracle database.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>An open Oracle connection. Caller is responsible for disposal.</returns>
|
||||
Task<OracleConnection> CreateJdeStageConnectionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens a connection to the CMS Oracle database.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>An open Oracle connection. Caller is responsible for disposal.</returns>
|
||||
Task<OracleConnection> CreateCmsConnectionAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Inventory (lots) operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial interface IJdeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets lot master data from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming lots.</returns>
|
||||
IAsyncEnumerable<Lot> GetLotsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets lot usage (cardex) transactions from production schema, optionally filtered by last update.
|
||||
/// Uses special LotUsageTimeoutSeconds timeout due to large dataset.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming lot usages.</returns>
|
||||
IAsyncEnumerable<LotUsage> GetLotUsagesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets lot usage transactions from archive schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming archived lot usages.</returns>
|
||||
IAsyncEnumerable<LotUsage> GetLotUsagesArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets lot location tracking from JDE Stage view.
|
||||
/// Uses JDE Stage connection.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming lot locations.</returns>
|
||||
IAsyncEnumerable<LotLocation> GetLotLocationsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Lookup;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Reference data operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial interface IJdeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets item master data from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming items.</returns>
|
||||
IAsyncEnumerable<Item> GetItemsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets user/operator data from production schema.
|
||||
/// Note: Incremental filtering not supported for users (full sync always).
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Ignored (full sync always).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming users.</returns>
|
||||
IAsyncEnumerable<JdeUser> GetUsersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets branch business units from production schema (type code 'BP').
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming branches.</returns>
|
||||
IAsyncEnumerable<Branch> GetBranchesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets profit center business units from production schema (type code 'I3').
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming profit centers.</returns>
|
||||
IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work center business units from production schema (type code 'WC').
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work centers.</returns>
|
||||
IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets status codes from JDE Stage view.
|
||||
/// Uses JDE Stage connection.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming status codes.</returns>
|
||||
IAsyncEnumerable<StatusCode> GetStatusCodesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets function codes from production schema.
|
||||
/// Note: Does not support incremental filtering (full sync always).
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming function codes.</returns>
|
||||
IAsyncEnumerable<FunctionCode> GetFunctionCodesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets organization hierarchy (work center to profit center mapping) from production schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming org hierarchy records.</returns>
|
||||
IAsyncEnumerable<OrgHierarchy> GetOrgHierarchyAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets item routing master data from production schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming route masters.</returns>
|
||||
IAsyncEnumerable<RouteMaster> GetRouteMastersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Work order operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial interface IJdeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets work orders from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work orders.</returns>
|
||||
IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work orders from archive schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming archived work orders.</returns>
|
||||
IAsyncEnumerable<WorkOrder> GetWorkOrdersArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order steps from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work order steps.</returns>
|
||||
IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order steps from archive schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming archived work order steps.</returns>
|
||||
IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order time transactions from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work order times.</returns>
|
||||
IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order time transactions from archive schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming archived work order times.</returns>
|
||||
IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order routing transactions from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work order routings.</returns>
|
||||
IAsyncEnumerable<WorkOrderRouting> GetWorkOrderRoutingsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order component usage from production schema, optionally filtered by last update.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming work order components.</returns>
|
||||
IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order component usage from archive schema.
|
||||
/// </summary>
|
||||
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Streaming archived work order components.</returns>
|
||||
IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for accessing JDE Oracle database.
|
||||
/// All methods return IAsyncEnumerable for memory-efficient streaming.
|
||||
/// </summary>
|
||||
public partial interface IJdeRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using JdeScoping.DataAccess.Models;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for building search queries using SqlKata.
|
||||
/// </summary>
|
||||
public interface ISearchQueryBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the main search query for flagging and retrieving work orders.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result with SQL, parameters, and setup statements.</returns>
|
||||
SearchQueryResult BuildSearchQuery(SearchModel model);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the MIS data extraction query when ExtractMisData is enabled.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result for MIS extraction.</returns>
|
||||
SearchQueryResult BuildMisQuery(SearchModel model);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the MIS non-match query for work orders without MIS records.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result for MIS non-match extraction.</returns>
|
||||
SearchQueryResult BuildMisNonMatchQuery(SearchModel model);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for traversing downstream work orders.
|
||||
/// </summary>
|
||||
public interface IWorkOrderTraversalService
|
||||
{
|
||||
/// <summary>
|
||||
/// Traverses downstream work orders via stored procedure or iterative SQL.
|
||||
/// Called after initial filtering to find related work orders.
|
||||
/// </summary>
|
||||
/// <param name="connection">The SQL connection with the #Temp_WO temp table already populated.</param>
|
||||
/// <param name="maxIterations">Maximum number of traversal iterations.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task TraverseDownstreamAsync(
|
||||
SqlConnection connection,
|
||||
int maxIterations = 20,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.26.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="SqlKata" Version="3.2.3" />
|
||||
<PackageReference Include="SqlKata.Execution" Version="3.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JdeScoping.Core\JdeScoping.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
@@ -0,0 +1,82 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.Results;
|
||||
|
||||
/// <summary>
|
||||
/// MIS non-match reporting model.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Investigation", TableName = "Investigation")]
|
||||
public sealed record MisNonMatchSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Work order job step work center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Center Code")]
|
||||
public string WorkCenterCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order start date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "Work Order Start Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime WorkOrderStartDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "Job Step Number")]
|
||||
public decimal JobStepNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Function Operation Description")]
|
||||
public string JobStepDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step completion date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "Job Step End Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? JobStepEndDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step function code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "Function Code")]
|
||||
public string FunctionCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the job step was added (not in original router).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 75, HeaderText = "Was Job Step Added?")]
|
||||
public bool WasJobStepAdded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Matched work order job step number (match to original router by work order number, work center code, and function code).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 76, HeaderText = "Matched Job Step Number")]
|
||||
public decimal? MatchedJobStepNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order router type.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Routing Type")]
|
||||
public string RoutingType { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.Results;
|
||||
|
||||
/// <summary>
|
||||
/// MIS data reporting model.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "MIS Info", TableName = "MIS_Info")]
|
||||
public sealed record MisSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Item unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "MIS Job Step Sequence Number")]
|
||||
public string SequenceNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "MIS Number")]
|
||||
public string MisNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS revision ID.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "MIS Revision")]
|
||||
public string RevId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS release status.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "MIS Release Status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS release date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "MIS Release Date", Format = OutputColumnAttribute.TimestampFormat)]
|
||||
public DateTime? ReleaseDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Branch unique code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Branch Code")]
|
||||
public string BranchCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Job Step Sequence Number")]
|
||||
public decimal JobStepSequenceNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Job step number for matched F3112Z1 / F3111 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Matched Sequence Number")]
|
||||
public decimal? MatchedSequenceNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the job step was matched to F3112Z1 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 110, HeaderText = "Matched to F3112Z1?")]
|
||||
public bool RoutingMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the job step was matched to F3111 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 120, HeaderText = "Matched to F3003?")]
|
||||
public bool MasterMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Job step function description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 130, HeaderText = "Function Operation Description")]
|
||||
public string FunctionOperationDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Characteristic number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 140, HeaderText = "Char Number")]
|
||||
public string CharNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Test description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 150, HeaderText = "Test Description", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string TestDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Type of sampling.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 160, HeaderText = "Sampling Type")]
|
||||
public string SamplingType { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Sampling selection value.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 170, HeaderText = "Sampling Value")]
|
||||
public string SamplingValue { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tools and gauges for MIS.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 180, HeaderText = "Tools & Gauges", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string ToolsGauges { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instructions for MIS.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 190, HeaderText = "Work Instructions", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string WorkInstructions { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.Results;
|
||||
|
||||
/// <summary>
|
||||
/// JDE search result reporting model.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Search Results", TableName = "Search_Results")]
|
||||
public sealed record SearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Order unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Order branch code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Work Order Branch Code")]
|
||||
public string WorkOrderBranchCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order lot number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "Lot Number")]
|
||||
public string LotNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item master planning family.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Planning Family")]
|
||||
public string PlanningFamily { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item master stocking type.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 55, HeaderText = "Stocking Type")]
|
||||
public string StockingType { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order quantity.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "Order Quantity")]
|
||||
public decimal OrderQuantity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity on hold.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "Held Quantity")]
|
||||
public decimal HeldQuantity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity scrapped/cancelled.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Scrapped Quantity")]
|
||||
public decimal ScrappedQuantity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity shipped.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Shipped Quantity")]
|
||||
public decimal ShippedQuantity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation branch code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Operation Step Branch Code")]
|
||||
public string StepBranchCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 110, HeaderText = "Operation Step")]
|
||||
public decimal StepNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation step description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 120, HeaderText = "Operation Step Description")]
|
||||
public string StepDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Function operation description (long text).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 130, HeaderText = "Function Operation Description")]
|
||||
public string FunctionOperationDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last update to operation step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 140, HeaderText = "Operation Step Update Timestamp", Format = OutputColumnAttribute.TimestampFormat)]
|
||||
public DateTime StepUpdateDt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Order status code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 150, HeaderText = "Status Code")]
|
||||
public string StatusCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order status description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 160, HeaderText = "Status Description")]
|
||||
public string StatusDescription { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last update to order status.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 170, HeaderText = "Status Update Timestamp", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? StatusUpdateDt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it was manually specified.
|
||||
/// </summary>
|
||||
public bool ManuallySpecified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it was split from a flagged work order.
|
||||
/// </summary>
|
||||
public bool SplitOrder { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it received parts from a flagged work order (CARDEX / F4111).
|
||||
/// </summary>
|
||||
public bool Cardex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it received parts from a flagged work order (parts list / F3111).
|
||||
/// </summary>
|
||||
public bool PartsList { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it met the filter criteria.
|
||||
/// </summary>
|
||||
public bool Flagged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason work order was included in results.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 180, HeaderText = "Inclusion Reason")]
|
||||
public string InclusionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ManuallySpecified)
|
||||
{
|
||||
return "ManuallySpecified";
|
||||
}
|
||||
if (Flagged)
|
||||
{
|
||||
return "Flagged";
|
||||
}
|
||||
if (Cardex && PartsList)
|
||||
{
|
||||
return "ComponentUsage (CARDEX + Parts List)";
|
||||
}
|
||||
if (Cardex && !PartsList)
|
||||
{
|
||||
return "ComponentUsage (CARDEX)";
|
||||
}
|
||||
if (!Cardex && PartsList)
|
||||
{
|
||||
return "ComponentUsage (Parts List)";
|
||||
}
|
||||
if (SplitOrder)
|
||||
{
|
||||
return "Split order";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using JdeScoping.DataAccess.Models.Results;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Reporting search data model.
|
||||
/// </summary>
|
||||
public class SearchModel
|
||||
{
|
||||
/// <summary>
|
||||
/// PK ID of search.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User name of user that created search.
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-friendly name for search.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was submitted.
|
||||
/// </summary>
|
||||
public DateTime? SubmitDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was started.
|
||||
/// </summary>
|
||||
public DateTime? StartDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was completed.
|
||||
/// </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>
|
||||
public List<SearchResult> Results { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// MIS results.
|
||||
/// </summary>
|
||||
public List<MisSearchResult> MisResults { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// MIS no match found results.
|
||||
/// </summary>
|
||||
public List<MisNonMatchSearchResult> MisNonMatchResults { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace JdeScoping.DataAccess.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of building a search query, containing the SQL and parameters.
|
||||
/// </summary>
|
||||
/// <param name="Sql">The compiled SQL query string.</param>
|
||||
/// <param name="Parameters">Dictionary of parameter names and values.</param>
|
||||
/// <param name="TempTableSetupSql">List of SQL statements for temp table setup (executed before main query).</param>
|
||||
public sealed record SearchQueryResult(
|
||||
string Sql,
|
||||
IDictionary<string, object> Parameters,
|
||||
IReadOnlyList<string> TempTableSetupSql);
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// SQL query constants for the CMS Oracle database.
|
||||
/// </summary>
|
||||
public static class CmsQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets Manufacturing Information System (MIS) data from CMS database.
|
||||
/// Complex 10-table JOIN through CMS schema (INFODBA).
|
||||
/// </summary>
|
||||
public const string SqlGetMisData = @"
|
||||
SELECT DISTINCT
|
||||
mis.P_PART_NUMBER AS ItemNumber,
|
||||
mis.P_OPERATION_NUMBER AS SequenceNumber,
|
||||
item.PITEM_ID AS MISNumber,
|
||||
itemrev.PITEM_REVISION_ID AS RevID,
|
||||
TRIM(mis.P_SITE) AS BranchCode,
|
||||
zim_test_details.P_SEQ_NUMBER AS CharNumber,
|
||||
zim_test_details.P_TEST_DESC AS TestDescription,
|
||||
zim_test_details.P_SAMPL_TYPE AS SamplingType,
|
||||
zim_test_details.P_SAMPL_VALUE AS SamplingValue,
|
||||
zim_test_details.P_TOOLS AS ToolsGauges,
|
||||
zim_test_details.P_WORK_INTR AS WorkInstructions,
|
||||
Status.PNAME AS Status,
|
||||
Status.PDATE_RELEASED AS ReleaseDate
|
||||
FROM INFODBA.PITEM item
|
||||
INNER JOIN INFODBA.PITEMREVISION itemrev ON (item.PUID = itemrev.RITEMS_TAGU)
|
||||
INNER JOIN INFODBA.PRELEASE_STATUS_LIST listing ON (itemrev.PUID = listing.PUID)
|
||||
INNER JOIN INFODBA.PRELEASESTATUS Status ON (listing.PVALU_0 = Status.PUID)
|
||||
INNER JOIN INFODBA.PIMANRELATION imanrel ON (itemrev.PUID = imanrel.RPRIMARY_OBJECTU)
|
||||
INNER JOIN INFODBA.PFORM form ON (imanrel.RSECONDARY_OBJECTU = form.PUID)
|
||||
INNER JOIN INFODBA.PZIMMERMISDETAILS zim_mis ON (form.RDATA_FILEU = zim_mis.PUID)
|
||||
INNER JOIN INFODBA.P_TEST_DETAILS test_details ON (zim_mis.PUID = test_details.PUID)
|
||||
INNER JOIN INFODBA.P_PART_ASSOCIATION ppa ON (ppa.PUID = test_details.PUID)
|
||||
INNER JOIN INFODBA.PMISDATAOBJECT mis ON (mis.PUID = ppa.PVALU_0)
|
||||
INNER JOIN INFODBA.PZIMTESTDETAILS zim_test_details ON (test_details.PVALU_0 = zim_test_details.PUID)
|
||||
WHERE Status.PNAME IN ('Current', 'BackLevel')";
|
||||
|
||||
/// <summary>
|
||||
/// Gets MIS data updated since specified date from CMS database.
|
||||
/// </summary>
|
||||
public const string SqlGetMisDataFiltered = SqlGetMisData + @"
|
||||
AND Status.PDATE_RELEASED >= :lastUpdateDT";
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Inventory related SQL queries (Lots, Lot Usages, Lot Locations, Items)
|
||||
/// </summary>
|
||||
public static partial class JdeQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all lots from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLots = @"
|
||||
SELECT TRIM(lot.IOLOTN) AS LotNumber,
|
||||
TRIM(lot.IOMCU) AS BranchCode,
|
||||
lot.IOITM AS ShortItemNumber,
|
||||
TRIM(lot.IOLITM) AS ItemNumber,
|
||||
lot.IOVEND AS SupplierCode,
|
||||
lot.IOLOTS AS StatusCode,
|
||||
TRIM(lot.IOLOT1) AS Memo1,
|
||||
TRIM(lot.IOLOT2) AS Memo2,
|
||||
TRIM(lot.IOLOT3) AS Memo3,
|
||||
lot.IOUPMJ AS LastUpdateDate,
|
||||
lot.IOTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F4108 lot
|
||||
WHERE TRIM(lot.IOLOTN) IS NOT NULL AND
|
||||
TRIM(lot.IOMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets lots updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotsFiltered = SqlGetLots + @"
|
||||
AND (lot.IOUPMJ > :dateUpdated OR
|
||||
(lot.IOUPMJ = :dateUpdated AND lot.IOTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all lot usages (cardex) from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotUsages = @"
|
||||
SELECT lu.ILUKID AS UniqueId,
|
||||
lu.ILDOCO AS WorkOrderNumber,
|
||||
TRIM(lu.ILLOTN) AS LotNumber,
|
||||
TRIM(lu.ILMCU) AS BranchCode,
|
||||
lu.ILITM AS ShortItemNumber,
|
||||
lu.ILTRQT AS Quantity,
|
||||
lu.ILTRDJ AS LastUpdateDate,
|
||||
lu.ILTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F4111 lu
|
||||
WHERE lu.ILDCT = 'IM' AND
|
||||
TRIM(lu.ILLOTN) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets lot usages updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotUsagesFiltered = SqlGetLotUsages + @"
|
||||
AND (lu.ILTRDJ > :dateUpdated OR
|
||||
(lu.ILTRDJ = :dateUpdated AND lu.ILTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all lot usages from archive schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotUsagesArchive = @"
|
||||
SELECT lu.ILUKID AS UniqueId,
|
||||
lu.ILDOCO AS WorkOrderNumber,
|
||||
TRIM(lu.ILLOTN) AS LotNumber,
|
||||
TRIM(lu.ILMCU) AS BranchCode,
|
||||
lu.ILITM AS ShortItemNumber,
|
||||
lu.ILTRQT AS Quantity,
|
||||
lu.ILTRDJ AS LastUpdateDate,
|
||||
lu.ILTDAY AS LastUpdateTime
|
||||
FROM {ArchiveSchema}.F4111 lu
|
||||
WHERE lu.ILDCT = 'IM' AND
|
||||
TRIM(lu.ILLOTN) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all lot locations from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotLocations = @"
|
||||
SELECT TRIM(il.LILOTN) AS LotNumber,
|
||||
il.LIITM AS ShortItemNumber,
|
||||
TRIM(il.LIMCU) AS BranchCode,
|
||||
COALESCE(TRIM(il.LILOCN), ' ') AS Location,
|
||||
il.LIUPMJ AS LastUpdateDate,
|
||||
il.LITDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F41021 il
|
||||
WHERE TRIM(il.LILOTN) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets lot locations updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetLotLocationsFiltered = SqlGetLotLocations + @"
|
||||
AND (il.LIUPMJ > :dateUpdated OR
|
||||
(il.LIUPMJ = :dateUpdated AND il.LITDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetItems = @"
|
||||
SELECT pn.IMITM AS ShortItemNumber,
|
||||
TRIM(pn.IMLITM) AS ItemNumber,
|
||||
TRIM(pn.IMDSC1) AS Description,
|
||||
TRIM(pn.IMPRP4) AS PlanningFamily,
|
||||
TRIM(pn.IMSTKT) AS StockingType,
|
||||
pn.IMUPMJ AS LastUpdateDate,
|
||||
pn.IMTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F4101 pn
|
||||
WHERE TRIM(pn.IMLITM) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets items updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetItemsFiltered = SqlGetItems + @"
|
||||
AND (pn.IMUPMJ > :dateUpdated OR
|
||||
(pn.IMUPMJ = :dateUpdated AND pn.IMTDAY >= :timeUpdated))";
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Lookup related SQL queries (Status Codes, Function Codes)
|
||||
/// </summary>
|
||||
public static partial class JdeQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all work order status codes from production schema.
|
||||
/// Status codes are stored in UDC table F0005 with SY='00' and RT='SS'.
|
||||
/// </summary>
|
||||
public const string SqlGetStatusCodes = @"
|
||||
SELECT TRIM(sc.DRKY) AS Code,
|
||||
TRIM(sc.DRDL01) AS Description,
|
||||
sc.DRUPMJ AS LastUpdateDate,
|
||||
sc.DRUPMT AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F0005 sc
|
||||
WHERE TRIM(sc.DRSY) = '00' AND
|
||||
sc.DRRT = 'SS' AND
|
||||
TRIM(sc.DRKY) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets status codes updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetStatusCodesFiltered = SqlGetStatusCodes + @"
|
||||
AND (sc.DRUPMJ > :dateUpdated OR
|
||||
(sc.DRUPMJ = :dateUpdated AND sc.DRUPMT >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all function codes from production schema.
|
||||
/// Function codes are stored in F00192 (MES codes table).
|
||||
/// Uses LISTAGG to concatenate multiple descriptions for same code.
|
||||
/// </summary>
|
||||
public const string SqlGetFunctionCodes = @"
|
||||
SELECT Code,
|
||||
TRIM(LISTAGG(Description, ' ') WITHIN GROUP(ORDER BY Description) ||
|
||||
CASE WHEN MAX(total_lengthb) > 4000 THEN '...' ELSE '' END) AS Description,
|
||||
SYSDATE AS LastUpdateDt
|
||||
FROM (
|
||||
SELECT TRIM(fc.CFKY) AS Code,
|
||||
TRIM(ASCIISTR(fc.CFDS80)) AS Description,
|
||||
SUM(LENGTHB(TRIM(fc.CFDS80))+1) OVER(PARTITION BY TRIM(fc.CFKY) ORDER BY TRIM(fc.CFDS80)) - 1 cumul_lengthb,
|
||||
SUM(LENGTHB(TRIM(fc.CFDS80))+1) OVER(PARTITION BY TRIM(fc.CFKY)) - 1 total_lengthb,
|
||||
COUNT(*) OVER(PARTITION BY TRIM(fc.CFKY)) num_values
|
||||
FROM {ProductionSchema}.F00192 fc
|
||||
WHERE TRIM(fc.CFKY) IS NOT NULL
|
||||
)
|
||||
WHERE total_lengthb <= 4000 OR cumul_lengthb <= 4000 - length('...')
|
||||
GROUP BY Code";
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Organization related SQL queries (Users, Business Units, Org Hierarchy, Route Masters)
|
||||
/// </summary>
|
||||
public static partial class JdeQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all JDE users from production schema.
|
||||
/// Uses CTE to get unique users with most recent address book record.
|
||||
/// </summary>
|
||||
public const string SqlGetUsers = @"
|
||||
WITH USER_CTE AS (
|
||||
SELECT ab.ABAN8 AS AddressNumber,
|
||||
TRIM(pro.ULUSER) AS UserId,
|
||||
TRIM(ab.ABALPH) AS FullName,
|
||||
ab.ABUPMJ AS LastUpdateDate,
|
||||
ab.ABUPMT AS LastUpdateTime,
|
||||
ROW_NUMBER() OVER (PARTITION BY ab.ABAN8 ORDER BY ab.ABUPMJ DESC, ab.ABUPMT DESC) RN
|
||||
FROM {ProductionSchema}.F0101 ab
|
||||
LEFT OUTER JOIN {ProductionSchema}.F0092 pro ON (ab.ABAN8 = pro.ULAN8)
|
||||
WHERE ab.ABATE = 'Y'
|
||||
)
|
||||
SELECT AddressNumber,
|
||||
UserId,
|
||||
FullName,
|
||||
LastUpdateDate,
|
||||
LastUpdateTime
|
||||
FROM USER_CTE
|
||||
WHERE RN = 1";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all business units of specified type from production schema.
|
||||
/// Type codes: 'WC' = Work Center, 'PC' = Profit Center, 'BR' = Branch
|
||||
/// </summary>
|
||||
public const string SqlGetBusinessUnits = @"
|
||||
SELECT TRIM(wc.MCMCU) AS Code,
|
||||
TRIM(wc.MCDL01) AS Description,
|
||||
wc.MCUPMJ AS LastUpdateDate,
|
||||
wc.MCUPMT AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F0006 wc
|
||||
WHERE wc.MCSTYL = :typeCode";
|
||||
|
||||
/// <summary>
|
||||
/// Gets business units of specified type updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetBusinessUnitsFiltered = SqlGetBusinessUnits + @"
|
||||
AND (wc.MCUPMJ > :dateUpdated OR
|
||||
(wc.MCUPMJ = :dateUpdated AND wc.MCUPMT >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all organization hierarchy records from production schema.
|
||||
/// Maps work centers to profit centers and branches.
|
||||
/// </summary>
|
||||
public const string SqlGetOrgHierarchy = @"
|
||||
SELECT TRIM(oh.IWMCUW) AS ProfitCenterCode,
|
||||
TRIM(oh.IWMCU) AS WorkCenterCode,
|
||||
TRIM(oh.IWMMCU) AS BranchCode,
|
||||
oh.IWUPMJ AS LastUpdateDate,
|
||||
oh.IWTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F30006 oh
|
||||
WHERE TRIM(oh.IWMCU) IS NOT NULL AND
|
||||
TRIM(oh.IWMMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets org hierarchy records updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetOrgHierarchyFiltered = SqlGetOrgHierarchy + @"
|
||||
AND (oh.IWUPMJ > :dateUpdated OR
|
||||
(oh.IWUPMJ = :dateUpdated AND oh.IWTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all route masters from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetRouteMasters = @"
|
||||
SELECT TRIM(rm.IRMMCU) AS BranchCode,
|
||||
TRIM(rm.IRKITL) AS ItemNumber,
|
||||
TRIM(rm.IRTRT) AS RoutingType,
|
||||
rm.IROPSQ / 10.0 AS SequenceNumber,
|
||||
TRIM(rm.IRURRF) AS FunctionCode,
|
||||
TRIM(rm.IRMCU) AS WorkCenterCode,
|
||||
rm.IREFFF AS StartDateDate,
|
||||
rm.IREFFT AS EndDateDate,
|
||||
rm.IRUPMJ AS LastUpdateDate,
|
||||
rm.IRTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F3003 rm
|
||||
WHERE TRIM(rm.IRKITL) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets route masters updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetRouteMastersFiltered = SqlGetRouteMasters + @"
|
||||
AND (rm.IRUPMJ > :dateUpdated OR
|
||||
(rm.IRUPMJ = :dateUpdated AND rm.IRTDAY >= :timeUpdated))";
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Work order related SQL queries (Work Orders, Steps, Times, Routings, Components)
|
||||
/// </summary>
|
||||
public static partial class JdeQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all work orders from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorders = @"
|
||||
SELECT wo.WADOCO AS WorkOrderNumber,
|
||||
TRIM(wo.WAMMCU) AS BranchCode,
|
||||
TRIM(wo.WALOTN) AS LotNumber,
|
||||
TRIM(wo.WALITM) AS ItemNumber,
|
||||
wo.WAITM AS ShortItemNumber,
|
||||
TRIM(wo.WAPARS) AS ParentWorkOrderNumber,
|
||||
wo.WAUORG / 100.0 AS OrderQuantity,
|
||||
wo.WASOBK / 100.0 AS HeldQuantity,
|
||||
wo.WASOQS / 100.0 AS ShippedQuantity,
|
||||
TRIM(wo.WASRST) AS StatusCode,
|
||||
CASE wo.WADCG WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WADCG+1900000,'YYYYDDD') END AS StatusCodeUpdateDT,
|
||||
CASE wo.WATRDJ WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WATRDJ+1900000,'YYYYDDD') END AS IssueDate,
|
||||
CASE wo.WASTRT WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WASTRT+1900000,'YYYYDDD') END AS StartDate,
|
||||
TRIM(wo.WATRT) AS RoutingType,
|
||||
wo.WAUPMJ AS LastUpdateDate,
|
||||
wo.WATDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F4801 wo";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work orders updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkordersFiltered = SqlGetWorkorders + @"
|
||||
WHERE (wo.WAUPMJ > :dateUpdated OR
|
||||
(wo.WAUPMJ = :dateUpdated AND wo.WATDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all work orders from archive schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkordersArchive = @"
|
||||
SELECT wo.WADOCO AS WorkOrderNumber,
|
||||
TRIM(wo.WAMMCU) AS BranchCode,
|
||||
TRIM(wo.WALOTN) AS LotNumber,
|
||||
TRIM(wo.WALITM) AS ItemNumber,
|
||||
wo.WAITM AS ShortItemNumber,
|
||||
TRIM(wo.WAPARS) AS ParentWorkOrderNumber,
|
||||
wo.WAUORG / 100.0 AS OrderQuantity,
|
||||
wo.WASOBK / 100.0 AS HeldQuantity,
|
||||
wo.WASOQS / 100.0 AS ShippedQuantity,
|
||||
TRIM(wo.WASRST) AS StatusCode,
|
||||
CASE wo.WADCG WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WADCG+1900000,'YYYYDDD') END AS StatusCodeUpdateDT,
|
||||
CASE wo.WATRDJ WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WATRDJ+1900000,'YYYYDDD') END AS IssueDate,
|
||||
CASE wo.WASTRT WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
|
||||
ELSE TO_DATE(wo.WASTRT+1900000,'YYYYDDD') END AS StartDate,
|
||||
TRIM(wo.WATRT) AS RoutingType,
|
||||
wo.WAUPMJ AS LastUpdateDate,
|
||||
wo.WATDAY AS LastUpdateTime
|
||||
FROM {ArchiveSchema}.F4801 wo";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order steps from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderSteps = @"
|
||||
SELECT wos.WLDOCO AS WorkOrderNumber,
|
||||
wos.WLOPSQ/10 AS StepNumber,
|
||||
TRIM(wos.WLMCU) AS WorkCenterCode,
|
||||
TRIM(wos.WLMMCU) AS BranchCode,
|
||||
TRIM(wos.WLDSC1) AS StepDescription,
|
||||
TRIM(mes.CFDS80) AS FunctionOperationDescription,
|
||||
wos.WLOPSC AS StepTypeCode,
|
||||
CASE wos.WLSTRT WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wos.WLSTRT+1900000,'YYYYDDD') END AS StartDT,
|
||||
CASE wos.WLSTRX WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wos.WLSTRX+1900000,'YYYYDDD') END AS EndDT,
|
||||
TRIM(wos.WLURRF) AS FunctionCode,
|
||||
wos.WLSOCN / 100.0 AS ScrappedQuantity,
|
||||
wos.WLUPMJ AS LastUpdateDate,
|
||||
wos.WLTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F3112 wos
|
||||
LEFT OUTER JOIN {ProductionSchema}.F00192 mes ON (wos.WLURRF = mes.CFKY)
|
||||
WHERE TRIM(wos.WLMCU) IS NOT NULL AND
|
||||
TRIM(wos.WLMMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order steps updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderStepsFiltered = SqlGetWorkorderSteps + @"
|
||||
AND (wos.WLUPMJ > :dateUpdated OR
|
||||
(wos.WLUPMJ = :dateUpdated AND wos.WLTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order steps from archive schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderStepsArchive = @"
|
||||
SELECT wos.WLDOCO AS WorkOrderNumber,
|
||||
wos.WLOPSQ/10 AS StepNumber,
|
||||
TRIM(wos.WLMCU) AS WorkCenterCode,
|
||||
TRIM(wos.WLMMCU) AS BranchCode,
|
||||
TRIM(wos.WLDSC1) AS StepDescription,
|
||||
TRIM(mes.CFDS80) AS FunctionOperationDescription,
|
||||
wos.WLOPSC AS StepTypeCode,
|
||||
CASE wos.WLSTRT WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wos.WLSTRT+1900000,'YYYYDDD') END AS StartDT,
|
||||
CASE wos.WLSTRX WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wos.WLSTRX+1900000,'YYYYDDD') END AS EndDT,
|
||||
TRIM(wos.WLURRF) AS FunctionCode,
|
||||
wos.WLSOCN / 100.0 AS ScrappedQuantity,
|
||||
wos.WLUPMJ AS LastUpdateDate,
|
||||
wos.WLTDAY AS LastUpdateTime
|
||||
FROM {ArchiveSchema}.F3112 wos
|
||||
LEFT OUTER JOIN {ProductionSchema}.F00192 mes ON (wos.WLURRF = mes.CFKY)
|
||||
WHERE TRIM(wos.WLMCU) IS NOT NULL AND
|
||||
TRIM(wos.WLMMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order time transactions from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderTimes = @"
|
||||
SELECT wot.WTUKID AS UniqueID,
|
||||
wot.WTDOCO AS WorkOrderNumber,
|
||||
wot.WTOPSQ/10 AS StepNumber,
|
||||
TRIM(wot.WTMCU) AS WorkCenterCode,
|
||||
TRIM(wot.WTMMCU) AS BranchCode,
|
||||
wot.WTAN8 AS AddressNumber,
|
||||
CASE wot.WTDGL WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wot.WTDGL+1900000,'YYYYDDD') END AS GlDate,
|
||||
wot.WTUPMJ AS LastUpdateDate,
|
||||
wot.WTTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F31122 wot
|
||||
WHERE TRIM(wot.WTMCU) IS NOT NULL AND
|
||||
TRIM(wot.WTMMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order times updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderTimesFiltered = SqlGetWorkorderTimes + @"
|
||||
AND (wot.WTUPMJ > :dateUpdated OR
|
||||
(wot.WTUPMJ = :dateUpdated AND wot.WTTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order time transactions from archive schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderTimesArchive = @"
|
||||
SELECT wot.WTUKID AS UniqueID,
|
||||
wot.WTDOCO AS WorkOrderNumber,
|
||||
wot.WTOPSQ/10 AS StepNumber,
|
||||
TRIM(wot.WTMCU) AS WorkCenterCode,
|
||||
TRIM(wot.WTMMCU) AS BranchCode,
|
||||
wot.WTAN8 AS AddressNumber,
|
||||
CASE wot.WTDGL WHEN 0 THEN NULL
|
||||
ELSE TO_DATE(wot.WTDGL+1900000,'YYYYDDD') END AS GlDate,
|
||||
wot.WTUPMJ AS LastUpdateDate,
|
||||
wot.WTTDAY AS LastUpdateTime
|
||||
FROM {ArchiveSchema}.F31122 wot
|
||||
WHERE TRIM(wot.WTMCU) IS NOT NULL AND
|
||||
TRIM(wot.WTMMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order routing transactions from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderRoutings = @"
|
||||
SELECT TRIM(woz.SZEDUS) AS UserID,
|
||||
TRIM(woz.SZEDBT) AS BatchNumber,
|
||||
TRIM(woz.SZEDTN) AS TransactionNumber,
|
||||
woz.SZEDLN AS LineNumber,
|
||||
woz.SZOPSQ / 10.0 AS StepNumber,
|
||||
TRIM(woz.SZMCU) AS WorkCenterCode,
|
||||
woz.SZDOCO AS WorkOrderNumber,
|
||||
TRIM(woz.SZTRT) AS RoutingType,
|
||||
TRIM(woz.SZMMCU) AS BranchCode,
|
||||
TRIM(woz.SZDSC1) AS StepDescription,
|
||||
TRIM(woz.SZURRF) AS FunctionCode,
|
||||
woz.SZTRDJ AS TransactionDate_Date,
|
||||
woz.SZUPMJ AS LastUpdateDate,
|
||||
woz.SZTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F3112Z1 woz
|
||||
WHERE woz.SZTYTN = 'JDERTG' AND
|
||||
woz.SZDRIN = '2' AND
|
||||
woz.SZTNAC = '02' AND
|
||||
woz.SZPID = 'ER31410' AND
|
||||
TRIM(woz.SZEDUS) IS NOT NULL AND
|
||||
TRIM(woz.SZEDBT) IS NOT NULL AND
|
||||
TRIM(woz.SZEDTN) IS NOT NULL AND
|
||||
TRIM(woz.SZMCU) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order routings updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderRoutingsFiltered = SqlGetWorkorderRoutings + @"
|
||||
AND (woz.SZUPMJ > :dateUpdated OR
|
||||
(woz.SZUPMJ = :dateUpdated AND woz.SZTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order component usage from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderComponents = @"
|
||||
SELECT woc.WMUKID AS UniqueID,
|
||||
woc.WMDOCO AS WorkOrderNumber,
|
||||
TRIM(woc.WMLOTN) AS LotNumber,
|
||||
TRIM(woc.WMCMCU) AS BranchCode,
|
||||
woc.WMCPIT AS ShortItemNumber,
|
||||
woc.WMTRQT / 100.0 AS Quantity,
|
||||
woc.WMUPMJ AS LastUpdateDate,
|
||||
woc.WMTDAY AS LastUpdateTime
|
||||
FROM {ProductionSchema}.F3111 woc
|
||||
WHERE TRIM(woc.WMLOTN) IS NOT NULL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order components updated since specified date from production schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderComponentsFiltered = SqlGetWorkorderComponents + @"
|
||||
AND (woc.WMUPMJ > :dateUpdated OR
|
||||
(woc.WMUPMJ = :dateUpdated AND woc.WMTDAY >= :timeUpdated))";
|
||||
|
||||
/// <summary>
|
||||
/// Gets work order component usage from archive schema.
|
||||
/// </summary>
|
||||
public const string SqlGetWorkorderComponentsArchive = @"
|
||||
SELECT woc.WMUKID AS UniqueID,
|
||||
woc.WMDOCO AS WorkOrderNumber,
|
||||
TRIM(woc.WMLOTN) AS LotNumber,
|
||||
TRIM(woc.WMCMCU) AS BranchCode,
|
||||
woc.WMCPIT AS ShortItemNumber,
|
||||
woc.WMTRQT / 100.0 AS Quantity,
|
||||
woc.WMUPMJ AS LastUpdateDate,
|
||||
woc.WMTDAY AS LastUpdateTime
|
||||
FROM {ArchiveSchema}.F3111 woc
|
||||
WHERE TRIM(woc.WMLOTN) IS NOT NULL";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// SQL query constants for the JDE Oracle database.
|
||||
/// Schema placeholders ({ProductionSchema}, {ArchiveSchema}, {StageSchema}) are replaced at runtime.
|
||||
/// </summary>
|
||||
public static partial class JdeQueries
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Data sync and maintenance SQL queries
|
||||
/// </summary>
|
||||
public static partial class LotFinderQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the last data update record for each table/update type combination.
|
||||
/// </summary>
|
||||
public const string SqlGetLastDataUpdates = @"
|
||||
WITH DU_CTE AS (
|
||||
SELECT du.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY du.TableName, du.UpdateType ORDER BY du.StartDT DESC) RN
|
||||
FROM dbo.DataUpdate AS du
|
||||
)
|
||||
SELECT cte.SourceSystem,
|
||||
cte.SourceData,
|
||||
cte.TableName,
|
||||
cte.StartDT,
|
||||
cte.EndDT,
|
||||
cte.UpdateType,
|
||||
cte.WasSuccessful,
|
||||
cte.NumberRecords
|
||||
FROM DU_CTE cte
|
||||
WHERE cte.RN = 1";
|
||||
|
||||
/// <summary>
|
||||
/// Gets column metadata for a table.
|
||||
/// </summary>
|
||||
public const string SqlGetTableColumns = @"
|
||||
SELECT c.name AS Name,
|
||||
CASE t2.name
|
||||
WHEN 'varchar' THEN 'VARCHAR(' + CAST(c.max_length AS VARCHAR(10)) + ')'
|
||||
WHEN 'decimal' THEN 'DECIMAL(' + CAST(c.precision AS VARCHAR(4)) + ',' + CAST(c.scale AS VARCHAR(4)) + ')'
|
||||
ELSE UPPER(t2.name)
|
||||
END AS Definition
|
||||
FROM sys.columns c
|
||||
INNER JOIN sys.types AS t2 ON (c.system_type_id = t2.system_type_id)
|
||||
INNER JOIN sys.tables t ON (c.object_id = t.object_id)
|
||||
WHERE t.name = @name
|
||||
ORDER BY c.column_id";
|
||||
|
||||
/// <summary>
|
||||
/// Gets primary key columns for a table.
|
||||
/// </summary>
|
||||
public const string SqlGetTablePrimaryKey = @"
|
||||
SELECT COLUMN_NAME AS Name
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1 AND
|
||||
TABLE_NAME = @name
|
||||
ORDER BY ORDINAL_POSITION";
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds all indices on a table. Use string.Format to inject table name.
|
||||
/// </summary>
|
||||
public const string SqlRebuildIndices = "ALTER INDEX ALL ON {0} REBUILD WITH (FILLFACTOR = 95);";
|
||||
|
||||
/// <summary>
|
||||
/// Post-processing script to set MIS data obsoletion dates.
|
||||
/// </summary>
|
||||
public const string SqlPostprocessMisData = @"
|
||||
SET ANSI_WARNINGS OFF;
|
||||
|
||||
WITH cte AS (
|
||||
SELECT md.MisNumber, md.RevID, md.Status, MIN(md.ReleaseDate) Released
|
||||
FROM dbo.MisData AS md
|
||||
GROUP BY md.MisNumber, md.RevID, md.Status
|
||||
)
|
||||
UPDATE dbo.MisData
|
||||
SET ObsoleteDate = bl.Released
|
||||
FROM cte bl
|
||||
WHERE MisData.MisNumber = bl.MisNumber AND
|
||||
MisData.RevID = bl.RevID AND
|
||||
MisData.Status = 'Current' AND
|
||||
bl.Status = 'BackLevel';
|
||||
|
||||
WITH cte AS (
|
||||
SELECT md.MisNumber, md.RevID, md.Status, MIN(md.ReleaseDate) Released
|
||||
FROM dbo.MisData AS md
|
||||
GROUP BY md.MisNumber, md.RevID, md.Status
|
||||
)
|
||||
UPDATE dbo.MisData
|
||||
SET ObsoleteDate = (SELECT TOP 1 nl.Released
|
||||
FROM cte nl
|
||||
WHERE MisData.MisNumber = nl.MisNumber AND
|
||||
MisData.RevID < nl.RevID AND
|
||||
MisData.Status = nl.Status
|
||||
ORDER BY nl.RevID)
|
||||
WHERE ObsoleteDate IS NULL;
|
||||
|
||||
ALTER INDEX [PK_MisData] ON [dbo].[MisData] REBUILD;";
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new data update tracking record.
|
||||
/// </summary>
|
||||
public const string SqlInsertDataUpdate = @"
|
||||
INSERT INTO dbo.DataUpdate (SourceSystem, SourceData, TableName, StartDT, UpdateType)
|
||||
VALUES (@sourceSystem, @sourceData, @tableName, @startDT, @updateType);
|
||||
SELECT CAST(SCOPE_IDENTITY() AS BIGINT);";
|
||||
|
||||
/// <summary>
|
||||
/// Completes a data update tracking record.
|
||||
/// </summary>
|
||||
public const string SqlCompleteDataUpdate = @"
|
||||
UPDATE dbo.DataUpdate
|
||||
SET EndDT = @endDT,
|
||||
WasSuccessful = @wasSuccessful,
|
||||
NumberRecords = @numberRecords
|
||||
WHERE ID = @id";
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Reference data lookup SQL queries
|
||||
/// </summary>
|
||||
public static partial class LotFinderQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches items by number or description.
|
||||
/// </summary>
|
||||
public const string SqlSearchItems = @"
|
||||
SELECT TOP 25
|
||||
i.ShortItemNumber,
|
||||
i.ItemNumber,
|
||||
i.Description,
|
||||
i.LastUpdateDT
|
||||
FROM dbo.Item AS i
|
||||
WHERE i.ItemNumber LIKE '%' + @filter + '%' OR
|
||||
i.Description LIKE '%' + @filter + '%'
|
||||
ORDER BY i.ItemNumber";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up items by exact item numbers using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupItems = @"
|
||||
SELECT i.ShortItemNumber,
|
||||
i.ItemNumber,
|
||||
i.Description,
|
||||
i.LastUpdateDT
|
||||
FROM dbo.Item AS i
|
||||
INNER JOIN @itemNumbers AS i2 ON (i.ItemNumber = i2.ItemNumber)
|
||||
ORDER BY i.ItemNumber";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up work orders by work order numbers using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupWorkorders = @"
|
||||
SELECT *
|
||||
FROM dbo.WorkOrder AS wo
|
||||
INNER JOIN @workOrderNumbers wo2 ON (wo.WorkOrderNumber = wo2.WorkOrderNumber)";
|
||||
|
||||
/// <summary>
|
||||
/// Searches work centers by code or description.
|
||||
/// </summary>
|
||||
public const string SqlSearchWorkCenters = @"
|
||||
SELECT TOP 25
|
||||
wc.Code,
|
||||
wc.Description,
|
||||
wc.LastUpdateDT
|
||||
FROM dbo.WorkCenter AS wc
|
||||
WHERE wc.Code LIKE '%' + @filter + '%' OR
|
||||
wc.Description LIKE '%' + @filter + '%'
|
||||
ORDER BY wc.Code";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up work centers by codes using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupWorkCenters = @"
|
||||
SELECT wc.Code,
|
||||
wc.Description,
|
||||
wc.LastUpdateDT
|
||||
FROM dbo.WorkCenter AS wc
|
||||
INNER JOIN @workCenterCodes wc2 ON (wc.Code = wc2.Code)
|
||||
ORDER BY wc.Code";
|
||||
|
||||
/// <summary>
|
||||
/// Searches profit centers by code or description.
|
||||
/// </summary>
|
||||
public const string SqlSearchProfitCenters = @"
|
||||
SELECT TOP 25
|
||||
pc.Code,
|
||||
pc.Description,
|
||||
pc.LastUpdateDT
|
||||
FROM dbo.ProfitCenter AS pc
|
||||
WHERE pc.Code LIKE '%' + @filter + '%' OR
|
||||
pc.Description LIKE '%' + @filter + '%'
|
||||
ORDER BY pc.Code";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up profit centers by codes using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupProfitCenters = @"
|
||||
SELECT pc.Code,
|
||||
pc.Description,
|
||||
pc.LastUpdateDT
|
||||
FROM dbo.ProfitCenter AS pc
|
||||
INNER JOIN @profitCenterCodes AS pc2 ON (pc.Code = pc2.Code)
|
||||
ORDER BY pc.Code";
|
||||
|
||||
/// <summary>
|
||||
/// Searches users by user ID, full name, or address number.
|
||||
/// </summary>
|
||||
public const string SqlSearchUsers = @"
|
||||
SELECT TOP 25
|
||||
u.AddressNumber,
|
||||
COALESCE(u.UserID,' ') AS UserID,
|
||||
u.FullName,
|
||||
u.LastUpdateDT
|
||||
FROM dbo.JdeUser AS u
|
||||
WHERE u.UserID LIKE '%' + @filter + '%' OR
|
||||
u.FullName LIKE '%' + @filter + '%' OR
|
||||
CAST(u.AddressNumber AS VARCHAR(10)) LIKE '%' + @filter + '%'
|
||||
ORDER BY u.UserID, u.FullName";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up users by user IDs or address numbers using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupUsers = @"
|
||||
SELECT u.AddressNumber,
|
||||
u.UserID,
|
||||
u.FullName,
|
||||
u.LastUpdateDT
|
||||
FROM dbo.JdeUser AS u
|
||||
INNER JOIN @userIDs u2 ON (u.UserID = u2.UserName OR CAST(u.AddressNumber AS VARCHAR(20)) = u2.UserName)
|
||||
ORDER BY u.UserID";
|
||||
|
||||
/// <summary>
|
||||
/// Looks up lots by lot number and item number using table-valued parameter.
|
||||
/// </summary>
|
||||
public const string SqlLookupLots = @"
|
||||
SELECT DISTINCT
|
||||
l.LotNumber,
|
||||
l.BranchCode,
|
||||
l.ShortItemNumber,
|
||||
l.ItemNumber,
|
||||
l.SupplierCode,
|
||||
l.LastUpdateDT
|
||||
FROM dbo.Lot AS l
|
||||
INNER JOIN @lotNumbers ln ON (l.LotNumber = ln.ComponentLotNumber AND
|
||||
((l.ItemNumber IS NULL AND ln.ItemNumber IS NULL) OR l.ItemNumber = ln.ItemNumber))";
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Search management SQL queries
|
||||
/// </summary>
|
||||
public static partial class LotFinderQueries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a user's searches (lightweight - no criteria or results).
|
||||
/// </summary>
|
||||
public const string SqlGetUserSearches = @"
|
||||
SELECT s.ID,
|
||||
s.Name,
|
||||
s.Status,
|
||||
s.SubmitDT,
|
||||
s.StartDT,
|
||||
s.EndDT
|
||||
FROM dbo.Search s
|
||||
WHERE s.UserName = @userName
|
||||
ORDER BY s.SubmitDT";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all queued searches (Status less than 3 = New, Submitted, Started).
|
||||
/// </summary>
|
||||
public const string SqlGetQueuedSearches = @"
|
||||
SELECT s.ID,
|
||||
s.UserName,
|
||||
s.Name,
|
||||
s.Status,
|
||||
s.SubmitDT,
|
||||
s.StartDT,
|
||||
s.EndDT
|
||||
FROM dbo.Search s
|
||||
WHERE s.Status < 3
|
||||
ORDER BY s.SubmitDT";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single search by ID with criteria.
|
||||
/// </summary>
|
||||
public const string SqlGetSearch = @"
|
||||
SELECT s.UserName,
|
||||
s.Name,
|
||||
s.Status,
|
||||
s.SubmitDT,
|
||||
s.StartDT,
|
||||
s.EndDT,
|
||||
s.Criteria as CriteriaJSON
|
||||
FROM dbo.Search s
|
||||
WHERE s.ID = @id";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the binary results for a search.
|
||||
/// </summary>
|
||||
public const string SqlGetSearchResults = @"
|
||||
SELECT s.Results
|
||||
FROM dbo.Search AS s
|
||||
WHERE s.ID = @id";
|
||||
|
||||
/// <summary>
|
||||
/// Updates search status.
|
||||
/// </summary>
|
||||
public const string SqlUpdateSearchStatus = @"
|
||||
UPDATE dbo.Search
|
||||
SET Status = @status,
|
||||
StartDT = CASE WHEN @status = 2 THEN GETUTCDATE() ELSE StartDT END,
|
||||
EndDT = CASE WHEN @status >= 3 THEN GETUTCDATE() ELSE EndDT END
|
||||
WHERE ID = @id";
|
||||
|
||||
/// <summary>
|
||||
/// Updates search results.
|
||||
/// </summary>
|
||||
public const string SqlUpdateSearchResults = @"
|
||||
UPDATE dbo.Search
|
||||
SET Results = @results
|
||||
WHERE ID = @id";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace JdeScoping.DataAccess.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// SQL query constants for the LotFinder SQL Server cache database.
|
||||
/// </summary>
|
||||
public static partial class LotFinderQueries
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.QueryBuilders;
|
||||
|
||||
/// <summary>
|
||||
/// Builds MIS extraction queries for work order step matching.
|
||||
/// </summary>
|
||||
public sealed class MisQueryBuilder
|
||||
{
|
||||
private readonly SqlServerCompiler _compiler;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of MisQueryBuilder.
|
||||
/// </summary>
|
||||
/// <param name="compiler">The SqlKata SQL Server compiler.</param>
|
||||
public MisQueryBuilder(SqlServerCompiler compiler)
|
||||
{
|
||||
_compiler = compiler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the complete MIS extraction SQL including temp table setup and data population.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The SQL statements for MIS extraction.</returns>
|
||||
public IReadOnlyList<string> BuildMisExtractionSql(SearchModel model)
|
||||
{
|
||||
var statements = new List<string>();
|
||||
|
||||
// Create #TempMisData temp table
|
||||
statements.Add(BuildTempMisDataTableSql());
|
||||
|
||||
// Build and execute MIS CTE query to populate temp table
|
||||
statements.Add(BuildMisCteSql(model));
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
private static string BuildTempMisDataTableSql()
|
||||
{
|
||||
return """
|
||||
IF OBJECT_ID('tempdb.dbo.#TempMisData', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #TempMisData;
|
||||
END
|
||||
CREATE TABLE #TempMisData (
|
||||
WorkOrderNumber BIGINT,
|
||||
ItemNumber VARCHAR(25),
|
||||
ItemDescription VARCHAR(30),
|
||||
BranchCode VARCHAR(12),
|
||||
WorkCenterCode VARCHAR(12),
|
||||
StepTimestamp DATETIME,
|
||||
SequenceNumber DECIMAL(7, 2),
|
||||
FunctionCode VARCHAR(15),
|
||||
FunctionOperationDescription VARCHAR(80),
|
||||
MatchedSequenceNumber DECIMAL(7, 2),
|
||||
RoutingMatch BIT,
|
||||
MasterMatch BIT,
|
||||
MisNumber VARCHAR(32),
|
||||
RevID VARCHAR(32),
|
||||
CharNumber VARCHAR(32),
|
||||
MisSequenceNumber VARCHAR(32),
|
||||
TestDescription VARCHAR(2000),
|
||||
SamplingType VARCHAR(32),
|
||||
SamplingValue VARCHAR(32),
|
||||
ToolsGauges VARCHAR(2000),
|
||||
WorkInstructions VARCHAR(2000),
|
||||
Status VARCHAR(32),
|
||||
ReleaseDate DATETIME
|
||||
);
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildMisCteSql(SearchModel model)
|
||||
{
|
||||
var joins = BuildMisJoins(model);
|
||||
var whereClause = BuildMisWhereClause(model);
|
||||
|
||||
return $"""
|
||||
WITH MIS_CTE AS(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.ItemNumber,
|
||||
wo.BranchCode,
|
||||
wo.RoutingType,
|
||||
wo.IssueDate,
|
||||
wos.WorkCenterCode,
|
||||
wos.StepNumber,
|
||||
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}
|
||||
)
|
||||
INSERT INTO #TempMISData
|
||||
(
|
||||
WorkOrderNumber,
|
||||
ItemNumber,
|
||||
ItemDescription,
|
||||
BranchCode,
|
||||
WorkCenterCode,
|
||||
StepTimestamp,
|
||||
SequenceNumber,
|
||||
FunctionCode,
|
||||
FunctionOperationDescription,
|
||||
MatchedSequenceNumber,
|
||||
RoutingMatch,
|
||||
MasterMatch,
|
||||
MisNumber,
|
||||
RevID,
|
||||
CharNumber,
|
||||
MisSequenceNumber,
|
||||
TestDescription,
|
||||
SamplingType,
|
||||
SamplingValue,
|
||||
ToolsGauges,
|
||||
WorkInstructions,
|
||||
Status,
|
||||
ReleaseDate
|
||||
)
|
||||
SELECT mm.WorkOrderNumber,
|
||||
mm.ItemNumber,
|
||||
mm.ItemDescription,
|
||||
mm.BranchCode,
|
||||
mm.WorkCenterCode,
|
||||
mm.StepTimestamp,
|
||||
mm.SequenceNumber,
|
||||
mm.FunctionCode,
|
||||
mm.FunctionOperationDescription,
|
||||
mm.MatchedSequenceNumber,
|
||||
mm.RoutingMatch,
|
||||
mm.MasterMatch,
|
||||
mm.MisNumber,
|
||||
mm.RevID,
|
||||
mm.CharNumber,
|
||||
mm.MisSequenceNumber,
|
||||
mm.TestDescription,
|
||||
mm.SamplingType,
|
||||
mm.SamplingValue,
|
||||
mm.ToolsGauges,
|
||||
mm.WorkInstructions,
|
||||
mm.Status,
|
||||
mm.ReleaseDate
|
||||
FROM MIS_CTE c CROSS APPLY
|
||||
dbo.MatchMIS(c.WorkOrderNumber, c.ItemNumber, c.BranchCode, c.RoutingType,
|
||||
c.IssueDate, c.WorkCenterCode, c.StepNumber, c.EndDT,
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.QueryBuilders;
|
||||
|
||||
/// <summary>
|
||||
/// SqlKata-based implementation of ISearchQueryBuilder.
|
||||
/// Builds SQL queries using a fluent query builder pattern.
|
||||
/// </summary>
|
||||
public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder
|
||||
{
|
||||
private readonly SqlServerCompiler _compiler;
|
||||
private readonly IEnumerable<IFilterHandler> _filterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of SqlKataSearchQueryBuilder.
|
||||
/// </summary>
|
||||
/// <param name="compiler">The SqlKata SQL Server compiler.</param>
|
||||
/// <param name="filterHandlers">Collection of filter handlers.</param>
|
||||
public SqlKataSearchQueryBuilder(
|
||||
SqlServerCompiler compiler,
|
||||
IEnumerable<IFilterHandler> filterHandlers)
|
||||
{
|
||||
_compiler = compiler;
|
||||
_filterHandlers = filterHandlers.OrderBy(h => h.Priority);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchQueryResult BuildSearchQuery(SearchModel model)
|
||||
{
|
||||
var setupStatements = new List<string>();
|
||||
var parameters = new Dictionary<string, object>();
|
||||
|
||||
// 1. Create the #Temp_WO temp table
|
||||
setupStatements.Add(BuildTempWoTableSql());
|
||||
|
||||
// 2. Apply filter handlers in priority order
|
||||
foreach (var handler in _filterHandlers.Where(h => h.IsEnabled(model)))
|
||||
{
|
||||
var filterResult = handler.Apply(model, _compiler);
|
||||
setupStatements.AddRange(filterResult.SetupSql);
|
||||
foreach (var param in filterResult.Parameters)
|
||||
{
|
||||
parameters[param.Key] = param.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Build step-based flagging query if needed
|
||||
if (model.ShouldSearchSteps())
|
||||
{
|
||||
setupStatements.Add(BuildStepFlaggingQuery(model));
|
||||
}
|
||||
|
||||
// 4. Build the final result SELECT query
|
||||
var resultSql = BuildResultQuery();
|
||||
|
||||
return new SearchQueryResult(resultSql, parameters, setupStatements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchQueryResult BuildMisQuery(SearchModel model)
|
||||
{
|
||||
// MIS query is delegated to MisQueryBuilder
|
||||
// This is a placeholder - full implementation would generate MIS extraction SQL
|
||||
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;
|
||||
}
|
||||
|
||||
var sql = BuildMisExtractionQuery(model);
|
||||
return new SearchQueryResult(sql, parameters, []);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchQueryResult BuildMisNonMatchQuery(SearchModel model)
|
||||
{
|
||||
var sql = BuildMisNonMatchExtractionQuery();
|
||||
return new SearchQueryResult(sql, new Dictionary<string, object>(), []);
|
||||
}
|
||||
|
||||
private static string BuildTempWoTableSql()
|
||||
{
|
||||
return """
|
||||
--Setup flagged work order temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#Temp_WO', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #Temp_WO;
|
||||
END
|
||||
CREATE TABLE #Temp_WO (
|
||||
WorkOrderNumber BIGINT NOT NULL,
|
||||
LotNumber VARCHAR(30) NULL,
|
||||
BranchCode VARCHAR(12) NULL,
|
||||
ShortItemNumber BIGINT NOT NULL,
|
||||
ManuallySpecified BIT NOT NULL DEFAULT 0,
|
||||
SplitOrder BIT NOT NULL DEFAULT 0,
|
||||
CARDEX BIT NOT NULL DEFAULT 0,
|
||||
PartsList BIT NOT NULL DEFAULT 0,
|
||||
Flagged BIT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY CLUSTERED(WorkOrderNumber)
|
||||
);
|
||||
CREATE INDEX TIX_Temp_WO_Lookup ON #Temp_WO(LotNumber, BranchCode, ShortItemNumber);
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildStepFlaggingQuery(SearchModel model)
|
||||
{
|
||||
// Build the complex step-based flagging query dynamically
|
||||
var queryParts = new List<string>();
|
||||
|
||||
// First query part - WorkOrderStep based
|
||||
queryParts.Add(BuildStepSubquery(model, includeWorkOrderTime: true));
|
||||
|
||||
// Second query part - WorkOrderTime only (if not filtering by MIS)
|
||||
if (!model.ItemOperationMisFilterEnabled)
|
||||
{
|
||||
queryParts.Add(BuildTimeOnlySubquery(model));
|
||||
}
|
||||
|
||||
var unionQuery = string.Join("\r\n UNION \r\n", queryParts);
|
||||
|
||||
return $"""
|
||||
--Query data
|
||||
WITH LU_WO AS(
|
||||
SELECT DISTINCT
|
||||
step.WorkOrderNumber,
|
||||
step.LotNumber,
|
||||
step.BranchCode,
|
||||
step.ShortItemNumber
|
||||
FROM (
|
||||
{unionQuery}
|
||||
) step
|
||||
)
|
||||
MERGE INTO #Temp_WO AS TARGET
|
||||
USING LU_WO AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.Flagged = 1
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT(WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, Flagged)
|
||||
VALUES(SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildStepSubquery(SearchModel model, bool includeWorkOrderTime)
|
||||
{
|
||||
var joins = new List<string>();
|
||||
var whereClause = "";
|
||||
|
||||
// Item number filter join
|
||||
if (model.ItemNumberFilterEnabled)
|
||||
{
|
||||
joins.Add(" #P_ItemNumbers p_in ON (wo.ItemNumber = p_in.ItemNumber) INNER JOIN");
|
||||
}
|
||||
|
||||
// Base WorkOrderStep join
|
||||
joins.Add(" dbo.WorkOrderStep wos ON (wo.WorkOrderNumber = wos.WorkOrderNumber AND wo.BranchCode = wos.BranchCode) LEFT OUTER JOIN");
|
||||
joins.Add(" dbo.WorkOrderTime wot ON (wos.WorkOrderNumber = wot.WorkOrderNumber AND wos.StepNumber = wot.StepNumber AND wos.BranchCode = wot.BranchCode)");
|
||||
|
||||
// Work center filter join
|
||||
if (model.ProfitCenterFilterEnabled || model.WorkCenterFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_WorkCenters p_wc ON (wos.WorkCenterCode = p_wc.Code)");
|
||||
}
|
||||
|
||||
// Operator filter join
|
||||
if (model.OperatorFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_OperatorIDs p_oi ON (wot.AddressNumber = p_oi.AddressNumber)");
|
||||
}
|
||||
|
||||
// MIS filter join (uses CROSS APPLY)
|
||||
if (model.ItemOperationMisFilterEnabled)
|
||||
{
|
||||
joins.Add("""
|
||||
CROSS APPLY
|
||||
dbo.MatchMIS(wo.WorkOrderNumber, wo.ItemNumber, wo.BranchCode, wo.RoutingType, wo.IssueDate, wos.WorkCenterCode,
|
||||
wos.StepNumber, wos.EndDT, wos.FunctionCode, wos.FunctionOperationDescription) AS mm INNER JOIN
|
||||
#P_PartOperations p_po on (mm.ItemNumber = p_po.ItemNumber AND mm.MisSequenceNumber = p_po.OperationNumber AND mm.MisNumber = p_po.MisNumber AND mm.RevID = p_po.MisRevision)
|
||||
""");
|
||||
}
|
||||
|
||||
// Timespan WHERE clause
|
||||
if (model.TimespanFilterEnabled)
|
||||
{
|
||||
whereClause = """
|
||||
WHERE (wos.EndDT <= @p_MaximumDT AND wos.EndDT >= @p_MinimumDT) OR
|
||||
(wot.GlDate <= @p_MaximumDT AND wot.GlDate >= @p_MinimumDT)
|
||||
""";
|
||||
}
|
||||
|
||||
return $"""
|
||||
SELECT DISTINCT
|
||||
wo.WorkOrderNumber,
|
||||
COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrder wo INNER JOIN
|
||||
{string.Join("\r\n", joins)}
|
||||
{whereClause}
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildTimeOnlySubquery(SearchModel model)
|
||||
{
|
||||
var joins = new List<string>();
|
||||
var whereClause = "";
|
||||
|
||||
// Item number filter join
|
||||
if (model.ItemNumberFilterEnabled)
|
||||
{
|
||||
joins.Add(" #P_ItemNumbers p_in ON (wo.ItemNumber = p_in.ItemNumber) INNER JOIN");
|
||||
}
|
||||
|
||||
// Base WorkOrderTime join
|
||||
joins.Add(" dbo.WorkOrderTime wot ON (wo.WorkOrderNumber = wot.WorkOrderNumber AND wo.BranchCode = wot.BranchCode)");
|
||||
|
||||
// Work center filter join
|
||||
if (model.ProfitCenterFilterEnabled || model.WorkCenterFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_WorkCenters p_wc ON (wot.WorkCenterCode = p_wc.Code)");
|
||||
}
|
||||
|
||||
// Operator filter join
|
||||
if (model.OperatorFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_OperatorIDs p_oi ON (wot.AddressNumber = p_oi.AddressNumber)");
|
||||
}
|
||||
|
||||
// Timespan WHERE clause (only if both min and max are present)
|
||||
if (model.MinimumDt.HasValue && model.MaximumDt.HasValue)
|
||||
{
|
||||
whereClause = """
|
||||
WHERE (wot.GlDate <= @p_MaximumDT AND wot.GlDate >= @p_MinimumDT)
|
||||
""";
|
||||
}
|
||||
|
||||
return $"""
|
||||
SELECT DISTINCT
|
||||
wo.WorkOrderNumber,
|
||||
COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrder wo INNER JOIN
|
||||
{string.Join("\r\n", joins)}
|
||||
{whereClause}
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildResultQuery()
|
||||
{
|
||||
return """
|
||||
--Lookup flagged work order details
|
||||
WITH LAST_WOS AS
|
||||
(
|
||||
SELECT wos.WorkOrderNumber,
|
||||
wos.BranchCode,
|
||||
wos.StepNumber,
|
||||
wos.StepDescription,
|
||||
wos.FunctionOperationDescription,
|
||||
wos.LastUpdateDT,
|
||||
ROW_NUMBER() OVER (PARTITION BY wos.WorkOrderNumber, wos.BranchCode ORDER BY wos.EndDT DESC, wos.StepNumber DESC) AS RN
|
||||
FROM dbo.WorkOrderStep AS wos LEFT OUTER JOIN
|
||||
#Temp_WO t_wo ON (wos.WorkOrderNumber = t_wo.WorkOrderNumber AND wos.BranchCode = t_wo.BranchCode)
|
||||
)
|
||||
SELECT wo.WorkOrderNumber,
|
||||
wo.BranchCode AS WorkOrderBranchCode,
|
||||
wo.LotNumber,
|
||||
wo.ItemNumber,
|
||||
i.PlanningFamily,
|
||||
i.StockingType,
|
||||
t_wo.ManuallySpecified,
|
||||
t_wo.SplitOrder,
|
||||
t_wo.CARDEX,
|
||||
t_wo.PartsList,
|
||||
t_wo.Flagged,
|
||||
wo.OrderQuantity,
|
||||
wo.HeldQuantity,
|
||||
COALESCE(wots.TotalScrappedQuantity, 0) AS ScrappedQuantity,
|
||||
wo.ShippedQuantity,
|
||||
LTRIM(RTRIM(lwos.BranchCode)) AS StepBranchCode,
|
||||
lwos.StepNumber,
|
||||
lwos.StepDescription,
|
||||
lwos.FunctionOperationDescription,
|
||||
lwos.LastUpdateDT AS StepUpdateDT,
|
||||
wo.StatusCode,
|
||||
sc.Description AS StatusDescription,
|
||||
wo.StatusCodeUpdateDT AS StatusUpdateDT
|
||||
FROM dbo.WorkOrder AS wo INNER JOIN
|
||||
#Temp_WO AS t_wo ON (t_wo.WorkOrderNumber = wo.WorkOrderNumber AND t_wo.BranchCode = wo.BranchCode) LEFT OUTER JOIN
|
||||
dbo.Item i on (wo.ShortItemNumber = i.ShortItemNumber) INNER JOIN
|
||||
dbo.StatusCode AS sc ON (sc.Code = wo.StatusCode) LEFT OUTER JOIN
|
||||
LAST_WOS lwos ON (wo.WorkOrderNumber = lwos.WorkOrderNumber AND wo.BranchCode = lwos.BranchCode) LEFT OUTER JOIN
|
||||
dbo.WorkOrderTotalScrap wots ON (wo.WorkOrderNumber = wots.WorkOrderNumber AND wo.BranchCode = wots.BranchCode)
|
||||
WHERE lwos.RN IS NULL OR
|
||||
lwos.RN = 1;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildMisExtractionQuery(SearchModel model)
|
||||
{
|
||||
return """
|
||||
--Get MIS search results
|
||||
SELECT DISTINCT tmd.ItemNumber,
|
||||
tmd.ItemDescription,
|
||||
tmd.BranchCode,
|
||||
tmd.MisSequenceNumber AS SequenceNumber,
|
||||
tmd.FunctionCode,
|
||||
tmd.FunctionOperationDescription,
|
||||
tmd.SequenceNumber AS JobStepSequenceNumber,
|
||||
tmd.MatchedSequenceNumber,
|
||||
tmd.RoutingMatch,
|
||||
tmd.MasterMatch,
|
||||
tmd.MisNumber,
|
||||
tmd.RevID,
|
||||
tmd.CharNumber,
|
||||
tmd.TestDescription,
|
||||
tmd.SamplingType,
|
||||
tmd.SamplingValue,
|
||||
tmd.ToolsGauges,
|
||||
tmd.WorkInstructions,
|
||||
tmd.Status,
|
||||
tmd.ReleaseDate
|
||||
FROM #TempMisData AS tmd
|
||||
ORDER BY tmd.ItemNumber,
|
||||
tmd.BranchCode,
|
||||
tmd.SequenceNumber,
|
||||
tmd.MatchedSequenceNumber;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildMisNonMatchExtractionQuery()
|
||||
{
|
||||
return """
|
||||
--Get no-match MIS search results
|
||||
SELECT DISTINCT tmd.WorkCenterCode,
|
||||
tmd.WorkOrderNumber,
|
||||
wo.IssueDate AS WorkOrderStartDate,
|
||||
tmd.SequenceNumber AS JobStepNumber,
|
||||
tmd.FunctionOperationDescription AS JobStepDescription,
|
||||
tmd.StepTimestamp AS JobStepEndDate,
|
||||
CASE WHEN wo.RoutingType='NMR' OR NOT EXISTS(SELECT * FROM dbo.WorkOrderRouting wor WHERE tmd.WorkOrderNumber = wor.WorkOrderNumber AND wor.StepNumber = tmd.SequenceNumber) THEN 1 ELSE 0 END AS WasJobStepAdded,
|
||||
CASE WHEN wo.RoutingType='NMR' THEN NULL ELSE (SELECT TOP 1 wor.StepNumber FROM dbo.WorkOrderRouting wor WHERE (tmd.WorkOrderNumber = wor.WorkOrderNumber AND tmd.WorkCenterCode = wor.WorkCenterCode AND tmd.FunctionCode = wor.FunctionCode) AND wor.StepNumber <> tmd.SequenceNumber) END AS MatchedJobStepNumber,
|
||||
tmd.FunctionCode,
|
||||
tmd.ItemNumber,
|
||||
i.Description AS ItemDescription,
|
||||
wo.RoutingType
|
||||
FROM #TempMisData AS tmd INNER JOIN
|
||||
dbo.WorkOrder AS wo ON (tmd.WorkOrderNumber = wo.WorkOrderNumber AND tmd.BranchCode = wo.BranchCode) LEFT OUTER JOIN
|
||||
dbo.Item AS i ON (wo.ShortItemNumber = i.ShortItemNumber)
|
||||
WHERE (tmd.RoutingMatch = 0 AND
|
||||
tmd.MasterMatch = 0) OR
|
||||
tmd.MisNumber IS NULL
|
||||
ORDER BY tmd.WorkOrderNumber,
|
||||
tmd.SequenceNumber;
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Quality;
|
||||
using JdeScoping.DataAccess.Configuration;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for the CMS Oracle database.
|
||||
/// </summary>
|
||||
public class CmsRepository : ICmsRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<CmsRepository> _logger;
|
||||
private readonly IOptions<DataAccessOptions> _options;
|
||||
private const string RepositoryName = "CmsRepository";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CmsRepository"/> class.
|
||||
/// </summary>
|
||||
public CmsRepository(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ILogger<CmsRepository> logger,
|
||||
IOptions<DataAccessOptions> options)
|
||||
{
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<MisData> GetMisDataAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = lastUpdateDt.HasValue
|
||||
? CmsQueries.SqlGetMisDataFiltered
|
||||
: CmsQueries.SqlGetMisData;
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { lastUpdateDT = lastUpdateDt.Value }
|
||||
: null;
|
||||
|
||||
OracleConnection? connection = null;
|
||||
try
|
||||
{
|
||||
connection = await _connectionFactory.CreateCmsConnectionAsync(ct);
|
||||
|
||||
// Use Query with buffered: false for streaming
|
||||
var results = connection.Query<MisData>(
|
||||
sql,
|
||||
parameters,
|
||||
commandTimeout: _options.Value.MisDataTimeoutSeconds,
|
||||
buffered: false);
|
||||
|
||||
foreach (var item in results)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Convert ReleaseDate to local time if present
|
||||
if (item.ReleaseDate.HasValue)
|
||||
{
|
||||
item.ReleaseDate = item.ReleaseDate.Value.ToLocalTime();
|
||||
}
|
||||
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using JdeScoping.Core.Helpers;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Inventory (lots) operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial class JdeRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Lot> GetLotsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetLotsFiltered
|
||||
: JdeQueries.SqlGetLots);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<Lot>(
|
||||
sql, parameters, nameof(GetLotsAsync), "SQL_GET_LOTS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<LotUsage> GetLotUsagesAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetLotUsagesFiltered
|
||||
: JdeQueries.SqlGetLotUsages);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
// Use special lot usage timeout due to large dataset
|
||||
await foreach (var item in StreamQueryAsync<LotUsage>(
|
||||
sql, parameters, nameof(GetLotUsagesAsync), "SQL_GET_LOT_USAGES", ct,
|
||||
_options.Value.LotUsageTimeoutSeconds))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<LotUsage> GetLotUsagesArchiveAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetLotUsagesArchive);
|
||||
|
||||
// Use special lot usage timeout due to large dataset
|
||||
await foreach (var item in StreamQueryAsync<LotUsage>(
|
||||
sql, null, nameof(GetLotUsagesArchiveAsync), "SQL_GET_LOT_USAGES_ARCHIVE", ct,
|
||||
_options.Value.LotUsageTimeoutSeconds))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<LotLocation> GetLotLocationsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetLotLocationsFiltered
|
||||
: JdeQueries.SqlGetLotLocations);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { lastUpdateDT = lastUpdateDt.Value }
|
||||
: null;
|
||||
|
||||
// Use JDE Stage connection for lot locations
|
||||
await foreach (var item in StreamQueryFromStageAsync<LotLocation>(
|
||||
sql, parameters, nameof(GetLotLocationsAsync), "SQL_GET_LOT_LOCATIONS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using JdeScoping.Core.Helpers;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Lookup;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Reference data operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial class JdeRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Item> GetItemsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetItemsFiltered
|
||||
: JdeQueries.SqlGetItems);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<Item>(
|
||||
sql, parameters, nameof(GetItemsAsync), "SQL_GET_ITEMS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<JdeUser> GetUsersAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
// Users always do full sync (incremental not supported)
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetUsers);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<JdeUser>(
|
||||
sql, null, nameof(GetUsersAsync), "SQL_GET_USERS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Branch> GetBranchesAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetBusinessUnitsFiltered
|
||||
: JdeQueries.SqlGetBusinessUnits);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { typeCode = "BP", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: new { typeCode = "BP", dateUpdated = 0, timeUpdated = 0 };
|
||||
|
||||
await foreach (var item in StreamQueryAsync<Branch>(
|
||||
sql, parameters, nameof(GetBranchesAsync), "SQL_GET_BUSINESS_UNITS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetBusinessUnitsFiltered
|
||||
: JdeQueries.SqlGetBusinessUnits);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { typeCode = "I3", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: new { typeCode = "I3", dateUpdated = 0, timeUpdated = 0 };
|
||||
|
||||
await foreach (var item in StreamQueryAsync<ProfitCenter>(
|
||||
sql, parameters, nameof(GetProfitCentersAsync), "SQL_GET_BUSINESS_UNITS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetBusinessUnitsFiltered
|
||||
: JdeQueries.SqlGetBusinessUnits);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { typeCode = "WC", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: new { typeCode = "WC", dateUpdated = 0, timeUpdated = 0 };
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkCenter>(
|
||||
sql, parameters, nameof(GetWorkCentersAsync), "SQL_GET_BUSINESS_UNITS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<StatusCode> GetStatusCodesAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetStatusCodesFiltered
|
||||
: JdeQueries.SqlGetStatusCodes);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { lastUpdateDT = lastUpdateDt.Value }
|
||||
: null;
|
||||
|
||||
// Use JDE Stage connection for status codes
|
||||
await foreach (var item in StreamQueryFromStageAsync<StatusCode>(
|
||||
sql, parameters, nameof(GetStatusCodesAsync), "SQL_GET_STATUS_CODES", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<FunctionCode> GetFunctionCodesAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetFunctionCodes);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<FunctionCode>(
|
||||
sql, null, nameof(GetFunctionCodesAsync), "SQL_GET_FUNCTION_CODES", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<OrgHierarchy> GetOrgHierarchyAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetOrgHierarchyFiltered
|
||||
: JdeQueries.SqlGetOrgHierarchy);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<OrgHierarchy>(
|
||||
sql, parameters, nameof(GetOrgHierarchyAsync), "SQL_GET_ORG_HIERARCHY", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<RouteMaster> GetRouteMastersAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetRouteMastersFiltered
|
||||
: JdeQueries.SqlGetRouteMasters);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<RouteMaster>(
|
||||
sql, parameters, nameof(GetRouteMastersAsync), "SQL_GET_ROUTE_MASTERS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using JdeScoping.Core.Helpers;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Work order operations for JDE Oracle repository.
|
||||
/// </summary>
|
||||
public partial class JdeRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetWorkordersFiltered
|
||||
: JdeQueries.SqlGetWorkorders);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrder>(
|
||||
sql, parameters, nameof(GetWorkOrdersAsync), "SQL_GET_WORKORDERS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersArchiveAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkordersArchive);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrder>(
|
||||
sql, null, nameof(GetWorkOrdersArchiveAsync), "SQL_GET_WORKORDERS_ARCHIVE", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetWorkorderStepsFiltered
|
||||
: JdeQueries.SqlGetWorkorderSteps);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderStep>(
|
||||
sql, parameters, nameof(GetWorkOrderStepsAsync), "SQL_GET_WORKORDER_STEPS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsArchiveAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderStepsArchive);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderStep>(
|
||||
sql, null, nameof(GetWorkOrderStepsArchiveAsync), "SQL_GET_WORKORDER_STEPS_ARCHIVE", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetWorkorderTimesFiltered
|
||||
: JdeQueries.SqlGetWorkorderTimes);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderTime>(
|
||||
sql, parameters, nameof(GetWorkOrderTimesAsync), "SQL_GET_WORKORDER_TIMES", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesArchiveAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderTimesArchive);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderTime>(
|
||||
sql, null, nameof(GetWorkOrderTimesArchiveAsync), "SQL_GET_WORKORDER_TIMES_ARCHIVE", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderRouting> GetWorkOrderRoutingsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetWorkorderRoutingsFiltered
|
||||
: JdeQueries.SqlGetWorkorderRoutings);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderRouting>(
|
||||
sql, parameters, nameof(GetWorkOrderRoutingsAsync), "SQL_GET_WORKORDER_ROUTINGS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(
|
||||
lastUpdateDt.HasValue
|
||||
? JdeQueries.SqlGetWorkorderComponentsFiltered
|
||||
: JdeQueries.SqlGetWorkorderComponents);
|
||||
|
||||
var parameters = lastUpdateDt.HasValue
|
||||
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
|
||||
: null;
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderComponent>(
|
||||
sql, parameters, nameof(GetWorkOrderComponentsAsync), "SQL_GET_WORKORDER_COMPONENTS", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsArchiveAsync(
|
||||
DateTime? lastUpdateDt = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderComponentsArchive);
|
||||
|
||||
await foreach (var item in StreamQueryAsync<WorkOrderComponent>(
|
||||
sql, null, nameof(GetWorkOrderComponentsArchiveAsync), "SQL_GET_WORKORDER_COMPONENTS_ARCHIVE", ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Configuration;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for the JDE Oracle database.
|
||||
/// </summary>
|
||||
public partial class JdeRepository : IJdeRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<JdeRepository> _logger;
|
||||
private readonly IOptions<DataAccessOptions> _options;
|
||||
private const string RepositoryName = "JdeRepository";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeRepository"/> class.
|
||||
/// </summary>
|
||||
public JdeRepository(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ILogger<JdeRepository> logger,
|
||||
IOptions<DataAccessOptions> options)
|
||||
{
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
private string ApplySchemaReplacements(string sql)
|
||||
{
|
||||
return sql
|
||||
.Replace("{ProductionSchema}", _options.Value.ProductionSchema)
|
||||
.Replace("{ArchiveSchema}", _options.Value.ArchiveSchema)
|
||||
.Replace("{StageSchema}", _options.Value.StageSchema);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<T> StreamQueryAsync<T>(
|
||||
string sql,
|
||||
object? parameters,
|
||||
string operation,
|
||||
string queryName,
|
||||
[EnumeratorCancellation] CancellationToken ct,
|
||||
int? timeoutSeconds = null)
|
||||
{
|
||||
OracleConnection? connection = null;
|
||||
try
|
||||
{
|
||||
connection = await _connectionFactory.CreateJdeConnectionAsync(ct);
|
||||
var timeout = timeoutSeconds ?? _options.Value.DefaultTimeoutSeconds;
|
||||
|
||||
// Use Query with buffered: false for streaming
|
||||
var results = connection.Query<T>(
|
||||
sql,
|
||||
parameters,
|
||||
commandTimeout: timeout,
|
||||
buffered: false);
|
||||
|
||||
foreach (var item in results)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<T> StreamQueryFromStageAsync<T>(
|
||||
string sql,
|
||||
object? parameters,
|
||||
string operation,
|
||||
string queryName,
|
||||
[EnumeratorCancellation] CancellationToken ct,
|
||||
int? timeoutSeconds = null)
|
||||
{
|
||||
OracleConnection? connection = null;
|
||||
try
|
||||
{
|
||||
connection = await _connectionFactory.CreateJdeStageConnectionAsync(ct);
|
||||
var timeout = timeoutSeconds ?? _options.Value.DefaultTimeoutSeconds;
|
||||
|
||||
// Use Query with buffered: false for streaming
|
||||
var results = connection.Query<T>(
|
||||
sql,
|
||||
parameters,
|
||||
commandTimeout: timeout,
|
||||
buffered: false);
|
||||
|
||||
foreach (var item in results)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Models.Infrastructure;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Data sync operations for LotFinder repository.
|
||||
/// </summary>
|
||||
public partial class LotFinderRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<DataUpdate>> GetLastDataUpdatesAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetLastDataUpdatesAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<DataUpdate>(
|
||||
LotFinderQueries.SqlGetLastDataUpdates,
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_LAST_DATA_UPDATES");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TableSpec> GetTableSpecAsync(string tableName, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetTableSpecAsync);
|
||||
try
|
||||
{
|
||||
var tableSpec = new TableSpec(tableName);
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
// Load columns
|
||||
var columns = await connection.QueryAsync<ColumnSpec>(
|
||||
LotFinderQueries.SqlGetTableColumns,
|
||||
new { name = tableName },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
tableSpec.Columns.AddRange(columns);
|
||||
|
||||
// Load primary key
|
||||
var pkColumns = await connection.QueryAsync<string>(
|
||||
LotFinderQueries.SqlGetTablePrimaryKey,
|
||||
new { name = tableName },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
foreach (var columnName in pkColumns)
|
||||
{
|
||||
var column = tableSpec.GetColumn(columnName);
|
||||
if (column != null)
|
||||
{
|
||||
tableSpec.PrimaryKey.Add(column);
|
||||
}
|
||||
}
|
||||
|
||||
return tableSpec;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_TABLE_COLUMNS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RebuildIndicesAsync(string tableName, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(RebuildIndicesAsync);
|
||||
|
||||
// Validate table name against whitelist (SQL injection prevention)
|
||||
if (!ValidTableNames.Contains(tableName))
|
||||
{
|
||||
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var sql = $"ALTER INDEX ALL ON [{tableName}] REBUILD WITH (FILLFACTOR = 95)";
|
||||
await connection.ExecuteAsync(sql, commandTimeout: _options.Value.RebuildIndexTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_REBUILD_INDICES");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PostProcessMisDataAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(PostProcessMisDataAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
await connection.ExecuteAsync(
|
||||
LotFinderQueries.SqlPostprocessMisData,
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_POSTPROCESS_MISDATA");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<int> BulkInsertAsync<T>(string tableName, IEnumerable<T> records, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(BulkInsertAsync);
|
||||
|
||||
// Validate table name against whitelist
|
||||
if (!ValidTableNames.Contains(tableName))
|
||||
{
|
||||
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
// Use SqlBulkCopy for efficient bulk insert
|
||||
using var bulkCopy = new SqlBulkCopy(connection)
|
||||
{
|
||||
DestinationTableName = $"dbo.[{tableName}]",
|
||||
BulkCopyTimeout = _options.Value.DefaultTimeoutSeconds
|
||||
};
|
||||
|
||||
// Convert records to DataTable
|
||||
var recordList = records.ToList();
|
||||
var dataTable = ToDataTable(recordList);
|
||||
|
||||
await bulkCopy.WriteToServerAsync(dataTable, ct);
|
||||
return recordList.Count;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "BulkInsert");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task TruncateTableAsync(string tableName, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(TruncateTableAsync);
|
||||
|
||||
// Validate table name against whitelist
|
||||
if (!ValidTableNames.Contains(tableName))
|
||||
{
|
||||
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var sql = $"TRUNCATE TABLE dbo.[{tableName}]";
|
||||
await connection.ExecuteAsync(sql, commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "TruncateTable");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Reference data lookup operations for LotFinder repository.
|
||||
/// </summary>
|
||||
public partial class LotFinderRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Item>> SearchItemsAsync(string filter, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(SearchItemsAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<Item>(
|
||||
LotFinderQueries.SqlSearchItems,
|
||||
new { filter },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_SEARCH_ITEMS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Item>> LookupItemsAsync(List<string> itemNumbers, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupItemsAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("ItemNumber", typeof(string));
|
||||
foreach (var itemNumber in itemNumbers)
|
||||
{
|
||||
dataTable.Rows.Add(itemNumber);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<Item>(
|
||||
LotFinderQueries.SqlLookupItems,
|
||||
new { itemNumbers = dataTable.AsTableValuedParameter("ItemNumberFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_ITEMS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<WorkOrder>> LookupWorkordersAsync(List<long> workorderNumbers, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupWorkordersAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("WorkOrderNumber", typeof(long));
|
||||
foreach (var workOrderNumber in workorderNumbers)
|
||||
{
|
||||
dataTable.Rows.Add(workOrderNumber);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<WorkOrder>(
|
||||
LotFinderQueries.SqlLookupWorkorders,
|
||||
new { workOrderNumbers = dataTable.AsTableValuedParameter("WorkOrderFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_WORKORDERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<WorkCenter>> SearchWorkCentersAsync(string filter, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(SearchWorkCentersAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<WorkCenter>(
|
||||
LotFinderQueries.SqlSearchWorkCenters,
|
||||
new { filter },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_SEARCH_WORK_CENTERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<WorkCenter>> LookupWorkCentersAsync(List<string> codes, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupWorkCentersAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("Code", typeof(string));
|
||||
foreach (var code in codes)
|
||||
{
|
||||
dataTable.Rows.Add(code);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<WorkCenter>(
|
||||
LotFinderQueries.SqlLookupWorkCenters,
|
||||
new { workCenterCodes = dataTable.AsTableValuedParameter("WorkCenterFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_WORK_CENTERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<ProfitCenter>> SearchProfitCentersAsync(string filter, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(SearchProfitCentersAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<ProfitCenter>(
|
||||
LotFinderQueries.SqlSearchProfitCenters,
|
||||
new { filter },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_SEARCH_PROFIT_CENTERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<ProfitCenter>> LookupProfitCentersAsync(List<string> codes, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupProfitCentersAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("Code", typeof(string));
|
||||
foreach (var code in codes)
|
||||
{
|
||||
dataTable.Rows.Add(code);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<ProfitCenter>(
|
||||
LotFinderQueries.SqlLookupProfitCenters,
|
||||
new { profitCenterCodes = dataTable.AsTableValuedParameter("ProfitCenterFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_PROFIT_CENTERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<JdeUser>> SearchUsersAsync(string filter, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(SearchUsersAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<JdeUser>(
|
||||
LotFinderQueries.SqlSearchUsers,
|
||||
new { filter },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_SEARCH_USERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<JdeUser>> LookupUsersAsync(List<string> userIds, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupUsersAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("UserName", typeof(string));
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
dataTable.Rows.Add(userId);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<JdeUser>(
|
||||
LotFinderQueries.SqlLookupUsers,
|
||||
new { userIDs = dataTable.AsTableValuedParameter("OperatorFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_USERS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Lot>> LookupLotsAsync(List<LotViewModel> lots, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(LookupLotsAsync);
|
||||
try
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("ComponentLotNumber", typeof(string));
|
||||
dataTable.Columns.Add("ItemNumber", typeof(string));
|
||||
foreach (var lot in lots)
|
||||
{
|
||||
dataTable.Rows.Add(lot.LotNumber, lot.ItemNumber);
|
||||
}
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<Lot>(
|
||||
LotFinderQueries.SqlLookupLots,
|
||||
new { lotNumbers = dataTable.AsTableValuedParameter("ComponentLotFilterParameter") },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_LOOKUP_LOTS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using JdeScoping.DataAccess.Queries;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Search management operations for LotFinder repository.
|
||||
/// </summary>
|
||||
public partial class LotFinderRepository
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Search>> GetUserSearchesAsync(string userName, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetUserSearchesAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<Search>(
|
||||
LotFinderQueries.SqlGetUserSearches,
|
||||
new { userName },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_USER_SEARCHES");
|
||||
throw; // Unreachable but satisfies compiler
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Search>> GetQueuedSearchesAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetQueuedSearchesAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryAsync<Search>(
|
||||
LotFinderQueries.SqlGetQueuedSearches,
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
return result.ToList();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_QUEUED_SEARCHES");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Search?> GetSearchAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetSearchAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
var result = await connection.QueryFirstOrDefaultAsync<Search>(
|
||||
LotFinderQueries.SqlGetSearch,
|
||||
new { id },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
result.Id = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_SEARCH");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<byte[]?> GetSearchResultsAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(GetSearchResultsAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
return await connection.QueryFirstOrDefaultAsync<byte[]>(
|
||||
LotFinderQueries.SqlGetSearchResults,
|
||||
new { id },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_GET_SEARCH_RESULTS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<int> SubmitSearchAsync(Search search, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(SubmitSearchAsync);
|
||||
try
|
||||
{
|
||||
search.Status = SearchStatus.Queued;
|
||||
search.SubmitDt = DateTime.UtcNow;
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
await using var command = new SqlCommand("SubmitSearch", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure,
|
||||
CommandTimeout = _options.Value.DefaultTimeoutSeconds
|
||||
};
|
||||
|
||||
command.Parameters.AddWithValue("p_UserName", search.UserName);
|
||||
command.Parameters.AddWithValue("p_Name", search.Name);
|
||||
command.Parameters.AddWithValue("p_Criteria", search.CriteriaJson);
|
||||
|
||||
var searchIdParam = new SqlParameter("o_SearchID", SqlDbType.Int)
|
||||
{
|
||||
Direction = ParameterDirection.Output
|
||||
};
|
||||
command.Parameters.Add(searchIdParam);
|
||||
|
||||
await command.ExecuteNonQueryAsync(ct);
|
||||
return Convert.ToInt32(searchIdParam.Value);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SubmitSearch");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task UpdateSearchStatusAsync(int id, SearchStatus status, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(UpdateSearchStatusAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
await connection.ExecuteAsync(
|
||||
LotFinderQueries.SqlUpdateSearchStatus,
|
||||
new { id, status = (int)status },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_UPDATE_SEARCH_STATUS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task UpdateSearchResultsAsync(int id, byte[] results, CancellationToken ct = default)
|
||||
{
|
||||
const string operation = nameof(UpdateSearchResultsAsync);
|
||||
try
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
await connection.ExecuteAsync(
|
||||
LotFinderQueries.SqlUpdateSearchResults,
|
||||
new { id, results },
|
||||
commandTimeout: _options.Value.DefaultTimeoutSeconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogAndThrow(ex, operation, "SQL_UPDATE_SEARCH_RESULTS");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Data;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.DataAccess.Configuration;
|
||||
using JdeScoping.DataAccess.Exceptions;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.DataAccess.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for the LotFinder SQL Server cache database.
|
||||
/// </summary>
|
||||
public partial class LotFinderRepository : ILotFinderRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<LotFinderRepository> _logger;
|
||||
private readonly IOptions<DataAccessOptions> _options;
|
||||
private const string RepositoryName = "LotFinderRepository";
|
||||
|
||||
/// <summary>
|
||||
/// Valid table names for index rebuild operations (SQL injection whitelist).
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> ValidTableNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Branch", "DataUpdate", "FunctionCode", "Item", "JdeUser",
|
||||
"Lot", "LotLocation", "LotUsage_Curr", "LotUsage_Hist",
|
||||
"MisData", "OrgHierarchy", "ProfitCenter", "RouteMaster",
|
||||
"Search", "StatusCode", "WorkCenter",
|
||||
"WorkOrder_Curr", "WorkOrder_Hist",
|
||||
"WorkOrderComponent_Curr", "WorkOrderComponent_Hist",
|
||||
"WorkOrderRouting",
|
||||
"WorkOrderStep_Curr", "WorkOrderStep_Hist",
|
||||
"WorkOrderTime_Curr", "WorkOrderTime_Hist"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LotFinderRepository"/> class.
|
||||
/// </summary>
|
||||
public LotFinderRepository(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ILogger<LotFinderRepository> logger,
|
||||
IOptions<DataAccessOptions> options)
|
||||
{
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
private void LogAndThrow(Exception ex, string operation, string queryName)
|
||||
{
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["Repository"] = RepositoryName,
|
||||
["Operation"] = operation,
|
||||
["QueryName"] = queryName
|
||||
}))
|
||||
{
|
||||
_logger.LogError(ex, "Query {QueryName} failed in {Operation}", queryName, operation);
|
||||
}
|
||||
|
||||
if (ex is SqlException sqlEx && IsTimeoutError(sqlEx))
|
||||
{
|
||||
throw new DataAccessTimeoutException(
|
||||
$"Query {queryName} timed out after {_options.Value.DefaultTimeoutSeconds} seconds.",
|
||||
_options.Value.DefaultTimeoutSeconds,
|
||||
operation,
|
||||
RepositoryName,
|
||||
ex);
|
||||
}
|
||||
|
||||
throw new QueryException(
|
||||
$"Query {queryName} failed: {ex.Message}",
|
||||
queryName,
|
||||
operation,
|
||||
RepositoryName,
|
||||
ex);
|
||||
}
|
||||
|
||||
private static bool IsTimeoutError(SqlException ex)
|
||||
{
|
||||
// SQL Server timeout error number: -2
|
||||
return ex.Number == -2;
|
||||
}
|
||||
|
||||
private static DataTable ToDataTable<T>(List<T> items)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
var properties = typeof(T).GetProperties()
|
||||
.Where(p => p.CanRead && IsSupportedType(p.PropertyType))
|
||||
.ToArray();
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
dataTable.Columns.Add(prop.Name, type);
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var row = dataTable.NewRow();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var value = prop.GetValue(item);
|
||||
row[prop.Name] = value ?? DBNull.Value;
|
||||
}
|
||||
dataTable.Rows.Add(row);
|
||||
}
|
||||
|
||||
return dataTable;
|
||||
}
|
||||
|
||||
private static bool IsSupportedType(Type type)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
return underlyingType.IsPrimitive
|
||||
|| underlyingType == typeof(string)
|
||||
|| underlyingType == typeof(DateTime)
|
||||
|| underlyingType == typeof(decimal)
|
||||
|| underlyingType == typeof(Guid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Configuration;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using JdeScoping.DataAccess.Models.Results;
|
||||
using JdeScoping.DataAccess.QueryBuilders;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Main search processor service that orchestrates search execution.
|
||||
/// </summary>
|
||||
public sealed class SearchProcessor
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ISearchQueryBuilder _queryBuilder;
|
||||
private readonly IWorkOrderTraversalService _traversalService;
|
||||
private readonly MisQueryBuilder _misQueryBuilder;
|
||||
private readonly SearchProcessingConfiguration _options;
|
||||
private readonly ILogger<SearchProcessor> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of SearchProcessor.
|
||||
/// </summary>
|
||||
public SearchProcessor(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ISearchQueryBuilder queryBuilder,
|
||||
IWorkOrderTraversalService traversalService,
|
||||
SqlServerCompiler compiler,
|
||||
IOptions<SearchProcessingConfiguration> options,
|
||||
ILogger<SearchProcessor> logger)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
_queryBuilder = queryBuilder;
|
||||
_traversalService = traversalService;
|
||||
_misQueryBuilder = new MisQueryBuilder(compiler);
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes search and returns results as async stream.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Async enumerable of search results.</returns>
|
||||
public async IAsyncEnumerable<SearchResult> ExecuteSearchAsync(
|
||||
SearchModel model,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogInformation("Executing search {SearchId}", model.Id);
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
// Build the search query
|
||||
var queryResult = _queryBuilder.BuildSearchQuery(model);
|
||||
|
||||
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
|
||||
{
|
||||
await WriteDebugSqlAsync(model.Id, queryResult);
|
||||
}
|
||||
|
||||
// Execute setup SQL (temp tables, filter population)
|
||||
foreach (var setupSql in queryResult.TempTableSetupSql)
|
||||
{
|
||||
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
|
||||
await connection.ExecuteAsync(
|
||||
setupSql,
|
||||
queryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
}
|
||||
|
||||
// Execute downstream traversal
|
||||
await _traversalService.TraverseDownstreamAsync(
|
||||
connection,
|
||||
_options.MaxTraversalIterations,
|
||||
ct);
|
||||
|
||||
// Stream results using unbuffered query
|
||||
_logger.LogDebug("Executing result query");
|
||||
var reader = await connection.QueryAsync<SearchResult>(
|
||||
queryResult.Sql,
|
||||
queryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
|
||||
foreach (var result in reader)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
yield return result;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Search {SearchId} completed", model.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes search and materializes all results into SearchModel.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The SearchModel populated with results.</returns>
|
||||
public async Task<SearchModel> ExecuteSearchToModelAsync(
|
||||
SearchModel model,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogInformation("Executing search {SearchId} to model", model.Id);
|
||||
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
// Build the search query
|
||||
var queryResult = _queryBuilder.BuildSearchQuery(model);
|
||||
|
||||
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
|
||||
{
|
||||
await WriteDebugSqlAsync(model.Id, queryResult);
|
||||
}
|
||||
|
||||
// Execute setup SQL (temp tables, filter population)
|
||||
foreach (var setupSql in queryResult.TempTableSetupSql)
|
||||
{
|
||||
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
|
||||
await connection.ExecuteAsync(
|
||||
setupSql,
|
||||
queryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
}
|
||||
|
||||
// Execute downstream traversal
|
||||
await _traversalService.TraverseDownstreamAsync(
|
||||
connection,
|
||||
_options.MaxTraversalIterations,
|
||||
ct);
|
||||
|
||||
// Execute result query and materialize
|
||||
_logger.LogDebug("Executing result query");
|
||||
var results = await connection.QueryAsync<SearchResult>(
|
||||
queryResult.Sql,
|
||||
queryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
|
||||
model.Results = results.ToList();
|
||||
_logger.LogInformation("Search {SearchId} returned {ResultCount} results", model.Id, model.Results.Count);
|
||||
|
||||
// Extract MIS data if requested
|
||||
if (model.ExtractMisData)
|
||||
{
|
||||
await ExecuteMisExtractionAsync(model, connection, ct);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private async Task ExecuteMisExtractionAsync(
|
||||
SearchModel model,
|
||||
SqlConnection connection,
|
||||
CancellationToken ct)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
foreach (var sql in misSetupStatements)
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
sql,
|
||||
misParameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
}
|
||||
|
||||
// Execute MIS result query
|
||||
var misQueryResult = _queryBuilder.BuildMisQuery(model);
|
||||
var misResults = await connection.QueryAsync<MisSearchResult>(
|
||||
misQueryResult.Sql,
|
||||
misQueryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
|
||||
model.MisResults = misResults.ToList();
|
||||
_logger.LogDebug("Found {MisResultCount} MIS results", model.MisResults.Count);
|
||||
|
||||
// Execute MIS non-match query
|
||||
var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model);
|
||||
var misNonMatchResults = await connection.QueryAsync<MisNonMatchSearchResult>(
|
||||
misNonMatchQueryResult.Sql,
|
||||
misNonMatchQueryResult.Parameters,
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
|
||||
model.MisNonMatchResults = misNonMatchResults.ToList();
|
||||
_logger.LogDebug("Found {MisNonMatchCount} MIS non-match results", model.MisNonMatchResults.Count);
|
||||
}
|
||||
|
||||
private async Task WriteDebugSqlAsync(int searchId, SearchQueryResult queryResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var debugPath = Path.Combine(_options.DebugSqlPath!, $"search_{searchId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.sql");
|
||||
var content = string.Join("\r\n\r\n-- ========================================\r\n\r\n",
|
||||
queryResult.TempTableSetupSql.Append(queryResult.Sql));
|
||||
await File.WriteAllTextAsync(debugPath, content);
|
||||
_logger.LogDebug("Debug SQL written to {Path}", debugPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to write debug SQL");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.DataAccess.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for traversing downstream work orders iteratively.
|
||||
/// </summary>
|
||||
public sealed class WorkOrderTraversalService : IWorkOrderTraversalService
|
||||
{
|
||||
private readonly ILogger<WorkOrderTraversalService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of WorkOrderTraversalService.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public WorkOrderTraversalService(ILogger<WorkOrderTraversalService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TraverseDownstreamAsync(
|
||||
SqlConnection connection,
|
||||
int maxIterations = 20,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Starting downstream work order traversal (max {MaxIterations} iterations)", maxIterations);
|
||||
|
||||
// The traversal SQL iteratively finds work orders that received material from flagged work orders
|
||||
const string traversalSql = """
|
||||
--Add downlevel work orders that were issued material from flagged work orders
|
||||
DECLARE @c_MAX_RUNS INT = @p_MaxIterations;
|
||||
DECLARE @v_NumWO INT = -1;
|
||||
DECLARE @v_NewNumWO INT;
|
||||
DECLARE @v_NumRuns INT = 0;
|
||||
|
||||
WHILE(1 = 1) BEGIN
|
||||
SET @v_NumWO = @v_NewNumWO;
|
||||
|
||||
--Add any work orders issued material from flagged work orders (parts list)
|
||||
WITH CWO_D AS(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrderComponent AS woc INNER JOIN
|
||||
dbo.WorkOrder AS wo ON (woc.WorkOrderNumber = wo.WorkOrderNumber) INNER JOIN
|
||||
#Temp_WO AS t_wo ON (woc.LotNumber = t_wo.LotNumber AND woc.ShortItemNumber = t_wo.ShortItemNumber)
|
||||
)
|
||||
MERGE #Temp_WO AS TARGET
|
||||
USING CWO_D AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.PartsList = 1
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, PartsList)
|
||||
VALUES (SOURCE.WorkOrderNumber, COALESCE(SOURCE.LotNumber, CAST(SOURCE.WorkOrderNumber AS VARCHAR(8))), SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
|
||||
--Add any work orders issued material from flagged work orders (CARDEX)
|
||||
WITH CWO_D AS(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.LotUsage AS lu INNER JOIN
|
||||
dbo.WorkOrder AS wo ON (lu.WorkOrderNumber = wo.WorkOrderNumber) INNER JOIN
|
||||
#Temp_WO AS t_wo ON (lu.LotNumber = t_wo.LotNumber AND lu.ShortItemNumber = t_wo.ShortItemNumber)
|
||||
)
|
||||
MERGE #Temp_WO AS TARGET
|
||||
USING CWO_D AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.CARDEX = 1
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, CARDEX)
|
||||
VALUES (SOURCE.WorkOrderNumber, COALESCE(SOURCE.LotNumber, CAST(SOURCE.WorkOrderNumber AS VARCHAR(8))), SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
|
||||
--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);
|
||||
|
||||
SELECT @v_NewNumWO = COUNT(*) FROM #Temp_WO;
|
||||
SET @v_NumRuns = @v_NumRuns + 1;
|
||||
|
||||
IF(@v_NumWO = @v_NewNumWO OR @v_NumRuns = @c_MAX_RUNS) BEGIN
|
||||
BREAK;
|
||||
END
|
||||
END;
|
||||
|
||||
SELECT @v_NumRuns AS IterationsCompleted, @v_NewNumWO AS TotalWorkOrders;
|
||||
""";
|
||||
|
||||
var result = await connection.QuerySingleAsync<(int IterationsCompleted, int TotalWorkOrders)>(
|
||||
traversalSql,
|
||||
new { p_MaxIterations = maxIterations },
|
||||
commandTimeout: 600);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Downstream traversal completed in {Iterations} iterations, found {TotalWorkOrders} total work orders",
|
||||
result.IterationsCompleted,
|
||||
result.TotalWorkOrders);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user