QR-Single Scanning

QR-Single Scanning

Flow

  • In Dncn1Create if sysparameter is On then we will make Qty field as readonly and add a placeholder Use Add Qty button below to add quantity
  • In Dncn1Create, if sysparameter is On then instead of Save button we will Add Qty button
  • Once user clicks on Add Qty then a drawer will get open, in that user can scan Trni-Ids, it will get stored in a UI table and then the Sum of Qty will be taken as Dncn.qty
  • Once the qty is added we will show submit button
  • Once submit button is clicked records will be create with existing flow
  • After that we will insert in scannedsubtrn and update qty in mfgsubtrn

Columns that will be shown in QR single scanning Drawer

Column Name
Trn Id
Sub Trn Id
Item Name
Qty
Delete

Validations

Validation Message Short Description
Qty must be between 0.001 and 999999999.999 Individual scanned item quantity must fall within the allowed numeric range.
IIRS is Un-Authorized The scanned TrnId is not authorized (IsAuth = “N”).
Stock is Rejected The scanned stock is marked as rejected (IsRejected = “Y”).
Entered Qty exceeds available balance Entered quantity is greater than the available balance quantity for the TrnId.
Item/Store/Stage/Brand of the Trnid is not matching with Invoice/Bill The scanned TrnId details (Item, Store, Stage, Brand) do not match the DNCN document configuration.
Party mismatch with DNCN The PartyId of the scanned TrnId does not match the DNCN PartyId.
Branch mismatch with DNCN The BranchId of the scanned TrnId does not match the DNCN BranchId.
Unit mismatch with DNCN The UnitCode of the scanned TrnId does not match the DNCN UnitCode.
Qty must be between 0.001 and 999999.999 Total aggregated quantity of all scanned items must be within allowed document range.
SCN/PCN qty cannot exceed the actual available qty for this Trn Id. For SCN or PCN type documents, the entered quantity cannot exceed actual available quantity for the specific TrnId.
Qty cannot be more than Invoice Qty / Bill Qty Total scanned quantity cannot exceed the original Invoice/Bill document quantity.
Invalid TrnId format. Expected format: M000001-001 (7-3 chars). QR format validation failed. TrnId must be 7 characters and SubTrnId must be 3 characters.
QR already scanned: {TrnId}-{SubTrnId} Duplicate QR scan detected in the current session.
No balance found or invalid QR for TrnId: {TrnId}, SubTrnId: {SubTrnId} No available balance returned from service or QR is invalid.
Failed to fetch quantity for {TrnId}-{SubTrnId}. Exception occurred while retrieving balance quantity from service.

Code

DncnQrDailog.razor
@inject IDncnService _IDncnService

<div class="d-flex flex-column" style="height: 100%;">
    <!-- Header Section: Total Quantity Display -->
    <div class="pa-4 flex-shrink-0" style="background-color: var(--mud-palette-surface);">
        <MudPaper Elevation="0" Outlined="true" Class="pa-4 mud-background-gray">
            <div class="d-flex justify-space-between align-center">
                <div>
                    <MudText Typo="Typo.subtitle2" Color="Color.Secondary">Total Scanned Quantity</MudText>
                    <MudText Typo="Typo.h4" Color="Color.Primary"><b>@_internalItems.Sum(x => x.Qty).ToString("N2")</b>
                    </MudText>
                </div>
                <div class="text-end">
                    <MudText Typo="Typo.subtitle2" Color="Color.Secondary">Total Scanned Items</MudText>
                    <MudText Typo="Typo.h6"><b>@_internalItems.Count</b></MudText>
                </div>
            </div>
        </MudPaper>

        <div class="mt-4">
            <MudTextField @ref="_qrTextField" Value="@qrInput"
                          ValueChanged="@(async (string s) => await OnQrTextChanged(s))" Label="Scan QR Code"
                          Variant="Variant.Outlined" Adornment="Adornment.End"
                          AdornmentIcon="@Icons.Material.Filled.QrCodeScanner" OnKeyDown="HandleQrKeyDown" AutoFocus="true"
                          Immediate="true" Placeholder="Please Scan here..." HelperText="Ensure scanner is focused here"
                          TextUpdateSuppression="false" />
        </div>
    </div>

    <!-- Body Section: Scanned Records Grid -->
    <div class="flex-grow-1 overflow-auto px-4 pb-4">
        @if (_internalItems.Any())
        {
            <MudTable Items="@_internalItems" Dense="true" Hover="true" Striped="true" Bordered="false" Elevation="0"
                      Class="border">
                <HeaderContent>
                    <MudTh Style="font-weight: bold;">TrnId</MudTh>
                    <MudTh Style="font-weight: bold;">SubTrnId</MudTh>
                    <MudTh Style="font-weight: bold;">ItemName</MudTh>
                    <MudTh Style="font-weight: bold; width: 100px;">Qty</MudTh>
                    <MudTh Style="font-weight: bold; width: 50px;" Class="text-center">Delete</MudTh>
                </HeaderContent>
                <RowTemplate>
                    <MudTd DataLabel="TrnId">@context.TrnId</MudTd>
                    <MudTd DataLabel="SubTrnId">@context.SubTrnId</MudTd>
                    <MudTd DataLabel="ItemName">@context.ItemName</MudTd>
                    <MudTd DataLabel="Qty">
                        <MudNumericField Value="@context.Qty" ValueChanged="@((decimal v) => UpdateItemQty(context, v))"
                                         Min="0m" Step="1m" HideSpinButtons="true" Variant="Variant.Text" Margin="Margin.Dense"
                                         Style="width: 80px;" Immediate="true" Error="@(!string.IsNullOrEmpty(context.QtyError))"
                                         ErrorText="@context.QtyError" />
                    </MudTd>
                    <MudTd DataLabel="Delete" Class="text-center">
                        <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
                                       OnClick="@(() => RemoveItem(context))" Size="Size.Small" />
                    </MudTd>
                </RowTemplate>
            </MudTable>
        }
        else
        {
            <div class="d-flex flex-column align-center justify-center mt-10" style="opacity: 0.4;">
                <MudIcon Icon="@Icons.Material.Filled.QrCodeScanner" Style="font-size: 5rem;" />
                <MudText Typo="Typo.h6">No items scanned yet</MudText>
                <MudText Typo="Typo.body2">Use a QR scanner</MudText>
            </div>
        }
    </div>

    <!-- Footer Section: Action Buttons -->
    <div class="pa-4 border-t flex-shrink-0 d-flex justify-space-between align-center"
         style="background-color: var(--mud-palette-surface);">
        <MudButton Variant="Variant.Text" Color="Color.Error" OnClick="ClearAll" Disabled="@(!_internalItems.Any())"
                   StartIcon="@Icons.Material.Filled.DeleteSweep">
            Clear All
        </MudButton>
        <div class="d-flex gap-2">
            <MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="OnCancelClick">Close</MudButton>
            <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnSubmitClick"
                       StartIcon="@Icons.Material.Filled.Save">
                Save
            </MudButton>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public List<QrItemModel> Items { get; set; } = new List<QrItemModel>();

    [Parameter]
    public EventCallback<List<QrItemModel>> OnCommit { get; set; }

    [Parameter]
    public EventCallback OnCancel { get; set; }

    [Parameter]
    public EventCallback<List<QrItemModel>> OnItemsChanged { get; set; }

    [Parameter]
    public Dncn1 ParentDncn1 { get; set; } = new Dncn1();

    [Parameter]
    public List<Dncn1> ReferenceLines { get; set; } = new List<Dncn1>();

    private List<QrItemModel> _internalItems = new List<QrItemModel>();
    private string qrInput = "";
    private MudTextField<string> _qrTextField;
    private PostLogin? _userInfo;

    protected override async Task OnInitializedAsync()
    {
        _userInfo = await _LocalSession.GetItemAsync<PostLogin>("userinfo");

        if (Items != null)
        {
            _internalItems = Items.Select(x => new QrItemModel
            {
                TrnId = x.TrnId,
                SubTrnId = x.SubTrnId,
                ItemName = x.ItemName,
                ItemId = x.ItemId,
                Qty = x.Qty,
                ScanTime = x.ScanTime,
                QtyError = x.QtyError
            }).ToList();

            foreach (var item in _internalItems)
            {
                item.QtyError = null;
            }
        }
    }

    private async Task OnCancelClick()
    {
        await OnCancel.InvokeAsync();
    }

    private async Task OnSubmitClick()
    {
        var dncnRequest = new DncnValidateQRRequest
        {
            Items = _internalItems,
            MainId = ParentDncn1.MainId,
            ItemId = ParentDncn1.ItemId,
            StoreCode = ParentDncn1.StoreCode,
            StageCode = ParentDncn1.StageCode,
            BrandId = ParentDncn1.BrandId,
            DocQty = ParentDncn1.DocQty
        };
        var response = await _IDncnService.Dncn1ValidateQR(dncnRequest, _userInfo?.dbname);

        if (response.StatusCode != HttpStatusCode.OK)
        {
            var errors = await response.Content.ReadFromJsonAsync<Dictionary<string, List<string>>>();
            if (errors != null)
            {
                foreach (var item in _internalItems) item.QtyError = null;

                foreach (var err in errors)
                {
                    // Pattern matches "Items[0].Qty"
                    if (err.Key.StartsWith("Items["))
                    {
                        var parts = err.Key.Split('[', ']');
                        if (parts.Length > 1 && int.TryParse(parts[1], out int index))
                        {
                            if (index >= 0 && index < _internalItems.Count)
                            {
                                _internalItems[index].QtyError = string.Join(", ", err.Value);
                            }
                        }
                    }
                }
                StateHasChanged();
            }
        }
        else
        {
            await OnCommit.InvokeAsync(_internalItems);
        }
    }

    private async Task ClearAll()
    {
        _internalItems.Clear();
        await NotifyChanges();
    }

    private async Task HandleQrKeyDown(KeyboardEventArgs args)
    {
        if (args.Key == "Enter")
        {
            await OnQrTextChanged(qrInput);
        }
    }

    private async Task OnQrTextChanged(string value)
    {
        qrInput = value;
        if (!string.IsNullOrWhiteSpace(qrInput))
        {
            var parts = qrInput.Split('-');

            // Strict Validation: TrnId (parts[0]) must be 7 characters, SubTrnId (parts[1]) must be 3 characters
            if (parts[0].Length != 7 || parts[1].Length != 3)
            {
                _ISnackbar.Add("Invalid TrnId format. Expected format: M000001-001 (7-3 chars).", Severity.Error);
                await ClearInput();
                return;
            }

            await ParseQrCode(qrInput);
            await ClearInput();
            StateHasChanged();
        }
    }

    private async Task ClearInput()
    {
        qrInput = "";
        if (_qrTextField != null)
        {
            await _qrTextField.Clear();
            await _qrTextField.FocusAsync();
        }
    }

    private async Task ParseQrCode(string input)
    {
        if (string.IsNullOrWhiteSpace(input)) return;
        var parts = input.Split('-');
        if (parts.Length >= 4)
        {
            var trnId = parts[0];
            var subTrnId = parts[1];

            // Check if already scanned to avoid duplicate calls/entries
            if (_internalItems.Any(x => x.TrnId == trnId && x.SubTrnId == subTrnId))
            {
                _ISnackbar.Add($"QR already scanned: {trnId}-{subTrnId}", Severity.Warning);
                return;
            }

            try
            {
                var balanceQty = await _IDncnService.GetQrSubTrnQty(trnId, subTrnId, _userInfo?.dbname);
                
                if (balanceQty <= 0)
                {
                    _ISnackbar.Add($"No balance found or invalid QR for TrnId: {trnId}, SubTrnId: {subTrnId}", Severity.Warning);
                    return;
                }

                var newItem = new QrItemModel
                {
                    TrnId = trnId,
                    SubTrnId = subTrnId,
                    ItemName = parts[2],
                    ItemId = parts[3],
                    Qty = balanceQty,
                    ScanTime = DateTime.Now
                };
                _internalItems.Insert(0, newItem);
                await NotifyChanges();
                StateHasChanged();
            }
            catch (Exception ex)
            {
                _ISnackbar.Add($"Failed to fetch quantity for {trnId}-{subTrnId}.", Severity.Error);
                Console.WriteLine($"Error parsing QR or fetching Qty: {ex.Message}");
            }
        }
    }

    private async Task UpdateItemQty(QrItemModel item, decimal value)
    {
        item.Qty = value;
        item.QtyError = null;
        await NotifyChanges();
    }

    private async Task RemoveItem(QrItemModel item)
    {
        _internalItems.Remove(item);
        await NotifyChanges();
    }


    private async Task NotifyChanges()
    {
        if (OnItemsChanged.HasDelegate)
        {
            await OnItemsChanged.InvokeAsync(_internalItems);
        }
        StateHasChanged();
    }

}
Dncn1ValidateQR Method
        [HttpPost("{dbname}")]
        public ActionResult Dncn1ValidateQR(DncnValidateQRRequest _Dncn1, string dbname)
        {
            var items = _Dncn1.Items;

            // Fetch main DNCN details using MainId
            var mainDncn = _IDncnRepository.GetDncnInfo(_Dncn1.MainId, dbname);

            string docType = mainDncn.DncnType.Substring(0, 1);
            string docName = docType == "S" ? "Invoice" : "Bill";

            // Duplicate scan detection (Rule H)
            var scanKeys = items.Select(x =>
            {
                var trnId = (x.TrnId ?? "").Trim();
                var subTrnId = (x.SubTrnId ?? "").Trim();
                return trnId + subTrnId;
            })
            .GroupBy(k => k)
            .Where(g => !string.IsNullOrEmpty(g.Key) && g.Count() > 1)
            .Select(g => g.Key)
            .ToHashSet();

            // Aggregate: total quantity only
            decimal totalQty = items.Sum(x => x.Qty);

            for (int i = 0; i < items.Count; i++)
            {
                var item = items[i];

                if (item.Qty < 0.001M || item.Qty > 999999999.999M)
                {
                    ModelState.AddModelError($"Items[{i}].Qty", "Qty must be between 0.001 and 999999999.999");
                    return BadRequest(ModelState);
                }

                var trnIdDetails = _IDncnRepository.GetTrnIdDetails(item.TrnId, item.SubTrnId, mainDncn.PartyId, mainDncn.BranchId, dbname);

                if (trnIdDetails.IsAuth == "N")
                {
                    ModelState.AddModelError($"Items[{i}].ItemId", "IIRS is Un-Authorized");
                    return BadRequest(ModelState);
                }

                if (trnIdDetails.IsRejected == "Y")
                {
                    ModelState.AddModelError($"Items[{i}].ItemId", "Stock is Rejected");
                    return BadRequest(ModelState);
                }

                if (item.Qty > trnIdDetails.BalanceQty)
                {
                    ModelState.AddModelError($"Items[{i}].Qty", "Entered Qty exceeds available balance");
                    return BadRequest(ModelState);
                }

                if (_Dncn1.ItemId != trnIdDetails.ItemId ||
                    _Dncn1.StoreCode != trnIdDetails.StoreCode ||
                    _Dncn1.StageCode != trnIdDetails.StageCode ||
                    _Dncn1.BrandId != trnIdDetails.BrandId)
                {
                    ModelState.AddModelError($"Items[{i}].ItemId",
                        $"Item/Store/Stage/Brand of the Trnid is not matching with {docName}");
                    return BadRequest(ModelState);
                }

                if (trnIdDetails.PartyId != "" && trnIdDetails.PartyId != mainDncn.PartyId)
                {
                    ModelState.AddModelError($"Items[{i}].Trnid", "Party mismatch with DNCN.");
                    return BadRequest(ModelState);
                }

                if (trnIdDetails.BranchId != "" && trnIdDetails.BranchId != mainDncn.BranchId)
                {
                    ModelState.AddModelError($"Items[{i}].Trnid", "Branch mismatch with DNCN.");
                    return BadRequest(ModelState);
                }

                if (trnIdDetails.UnitCode != mainDncn.UnitCode)
                {
                    ModelState.AddModelError($"Items[{i}].Trnid", "Unit mismatch with DNCN.");
                    return BadRequest(ModelState);
                }

                // Rule D/F: Aggregate quantity validation
                if (totalQty < 0.001M || totalQty > 999999.999M)
                {
                    ModelState.AddModelError($"Items[{i}].Qty", "Qty must be between 0.001 and 999999.999.");
                    return BadRequest(ModelState);
                }

                if(mainDncn.DncnType == "SCN" || mainDncn.DncnType == "PCN")
                {
                    var balanceQtyCnt = _IDncnRepository.GetBalanceQtyCntForSCNPCN(item.TrnId, item.SubTrnId, item.Qty, dbname);
                    if (balanceQtyCnt > 0)
                    {
                        ModelState.AddModelError($"Items[{i}].Qty", "SCN/PCN qty cannot exceed the actual available qty for this Trn Id.");
                        return BadRequest(ModelState);
                    }
                }
                else if (totalQty > _Dncn1.DocQty)
                {
                    ModelState.AddModelError($"Items[{i}].Qty", $"Qty cannot be more than {docName} Qty.");
                    return BadRequest(ModelState);
                }

            }

            return Ok(_Dncn1);
        }
Dncn Repository Methods
----dncn1 insert will happen through existing dncn1Create method 

---------------------------------GetTrnIdDetails---------------------------------
--This query will execute in loop for every trnid
WITH A(stage, storecode, brandid, partyid, unitcode, branchid, isauth, isrejected) AS
        (
            SELECT G1.ItemId,
            I1.stage AS stagecode, G1.storecode, I1.brandid, G.partyid, G.unitcode, G.branchid, 
            'Y' AS isauth, 'N' AS isrejected
            FROM Grn1 G1
            LEFT JOIN indent1 I1 ON G1.indent1id = I1.id
            LEFT JOIN Grn G ON G1.yeargrnno = G.YearGrnNo
            WHERE LEFT(Z.trnid,1) = 'G'

            UNION ALL

            SELECT J1.ItemId,
            J1.stage AS stagecode, J1.storecode, J1.brandid, J.partyid, J.unitcode, J.branchid, 
            'Y' AS isauth, 'N' AS isrejected
            FROM jobwork1 J1
            LEFT JOIN Jobwork J ON J1.yearjobno = J.YearJobNo
            WHERE LEFT(Z.trnid,1) = 'J'

            UNION ALL

            SELECT M1.ItemId,
            M1.stage AS stagecode, M1.storecode, M1.brandid, @partyid AS partyid, M.unitcode, @branchid AS branchid, 
            isauth, 'N' AS isrejected
            FROM Mirs1 M1
            LEFT JOIN Mirs M ON M1.yearjobno = M.yearjobno
            WHERE LEFT(Z.trnid,1) = 'M' 

            UNION ALL

            SELECT S.ItemId,
            S.stagecode, S.storecode, S.brandid, @partyid AS partyid, S.unitcode, @branchid AS branchid, 
            'Y' AS isauth, IIF(itemstate = 'G', 'N', 'Y') AS isrejected
            FROM Qrstock
            WHERE LEFT(Z.trnid,1) = 'S'

        )
        
-------------------GetBalanceQtyCntForSCNPCN------------------------

WITH A (trnid, subtrnid, qty) AS 
        (
            SELECT @trnid, @subtrnid, @qty
            
            UNION ALL 
            
            ----taking records which are already imported
            SELECT S.trnid, S.subtrnid, S.qty
            FROM scannedsubtrn S
            WHERE doctype IN ('SCN', 'PCN') AND isrolledback = 'N'
            AND S.trnid = @trnid AND S.subtrnid = @subtrnid

        ), 
        B (trnid, subtrnid, qty) AS 
        (
            SELECT trnid, subtrnid, SUM(qty) AS qty
            FROM A
            GROUP BY trnid, subtrnid
        ), 
        C(trnid, subtrnid) AS 
        (
            SELECT B.trnid, B.subtrnid
            FROM B
            LEFT JOIN QRsubtrn Q ON B.trnid = Q.trnid AND B.subtrnid = Q.subtrnid 
            WHERE (B.qty + Q.balanceqty) > Q.actualqty
        )
        SELECT COUNT(*) FROM C
        
        
------------------------Insert Into ScannedSubTrn------------------------
--This query will execute in loop for every trnid

INSERT INTO ScannedSubTrn (trnid, subtrnid, qty, yeardocno, doctype, docid, isrolledback)
VALUES (@trnid, @subtrnid, @qty, @yeardocno, @dncntype, @docid, 'N')

---------------------UpdateQrSubTrn-------------------------
--This query will execute in loop for every trnid

WITH A (trnid,subtrnid,qty) AS
        (
            SELECT @trnid,@subtrnid,IIF(@dncntype IN ('SCN', 'PCN'), @qty, @qty * (-1))
        )
        UPDATE QrSubTrn SET BalanceQty = BalanceQty + A.qty FROM A
        WHERE QrSubTrn.TrnId = A.TrnId AND QrSubTrn.SubTrnId = A.SubTrnId