unit frm_CalendarioProduccion;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls,
  Vcl.Buttons, Vcl.Grids, System.DateUtils, System.StrUtils, System.Types, System.UITypes,
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, uAppTheme, Vcl.Menus;

type
  TProduccionItem = record
    OrdenId: Integer;
    LineaId: Integer;
    NumeroOrden: string;
    ArticuloId: Integer;
    ArticuloNombre: string;
    Cantidad: Double;
    CantidadFabricada: Double;
    Estado: string;
    Prioridad: string;
    Observaciones: string;
    PedidoNumero: string;
    BoundsRect: TRect;
  end;

  TDiaProduccion = record
    Fecha: TDate;
    EsMesActual: Boolean;
    EsHoy: Boolean;
    TotalUnidades: Double;
    Items: TArray<TProduccionItem>;
    MasRect: TRect;
    TieneMas: Boolean;
  end;

  TfrmCalendarioProduccion = class(TForm)
    pnlTop: TPanel;
    pnlNav: TPanel;
    btnMesAnterior: TSpeedButton;
    btnMesSiguiente: TSpeedButton;
    btnHoy: TSpeedButton;
    cboMes: TComboBox;
    cboAnio: TComboBox;
    lblObrador: TLabel;
    cbbObrador: TComboBox;
    lblEstado: TLabel;
    cbbEstado: TComboBox;
    lblBuscar: TLabel;
    edtBuscar: TEdit;
    btnRefrescar: TSpeedButton;
    btnNuevaOrden: TSpeedButton;
    pnlDiasHeader: TPanel;
    pnlCalendarioContainer: TPanel;
    gridCalendario: TDrawGrid;
    pmCalendario: TPopupMenu;
    miNuevaOrden: TMenuItem;
    miEditarOrden: TMenuItem;
    miVerDetalleDia: TMenuItem;
    miEliminarOrden: TMenuItem;
    N1: TMenuItem;
    miRefrescar: TMenuItem;

    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure btnMesAnteriorClick(Sender: TObject);
    procedure btnMesSiguienteClick(Sender: TObject);
    procedure btnHoyClick(Sender: TObject);
    procedure cboPeriodoChange(Sender: TObject);
    procedure cbbFiltroChange(Sender: TObject);
    procedure edtBuscarKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure btnRefrescarClick(Sender: TObject);
    procedure btnNuevaOrdenClick(Sender: TObject);
    procedure gridCalendarioDrawCell(Sender: TObject; ACol, ARow: Longint;
      Rect: TRect; State: TGridDrawState);
    procedure gridCalendarioMouseMove(Sender: TObject; Shift: TShiftState;
      X, Y: Integer);
    procedure gridCalendarioDblClick(Sender: TObject);
    procedure gridCalendarioMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure miEditarOrdenClick(Sender: TObject);
    procedure miVerDetalleDiaClick(Sender: TObject);
    procedure miEliminarOrdenClick(Sender: TObject);
  private
    FCurrentYear: Word;
    FCurrentMonth: Word;
    FMatrizDias: array[0..41] of TDiaProduccion;
    FLastHoverItemIndex: Integer;
    FLastHoverDiaIndex: Integer;
    FLastHoverMasIndex: Integer;
    FSelectedOrdenId: Integer;
    FSelectedFecha: TDate;
    FLblDias: array[0..6] of TLabel;

    procedure InicializarFiltros;
    procedure CargarObradores;
    procedure CargarDatosMes;
    function ObtenerFechaDeCelda(ACol, ARow: Integer): TDate;
    function ItemBajoCursor(X, Y: Integer; var ADiaIdx, AItemIdx: Integer): Boolean;
    function MasBajoCursor(X, Y: Integer; var ADiaIdx: Integer): Boolean;
    function ConstruirHintMasArticulos(ADiaIdx: Integer): string;
    function ObtenerOrdenDeCelda(ACol, ARow, X, Y: Integer): Integer;
    procedure AjustarTamanosGrid;
    procedure AbrirDetalleDia(AFecha: TDate);
  public
  end;

var
  frmCalendarioProduccion: TfrmCalendarioProduccion;

implementation

uses
  dmg_Main, frm_OrdenProduccionEditor, frm_DiaDetalleProduccion;

{$R *.dfm}

const
  NOMBRES_MESES: array[1..12] of string = (
    'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
    'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
  );

  NOMBRES_DIAS: array[1..7] of string = (
    'LUNES', 'MARTES', 'MIÉRCOLES', 'JUEVES', 'VIERNES', 'SÁBADO', 'DOMINGO'
  );

procedure TfrmCalendarioProduccion.FormCreate(Sender: TObject);
var
  Y, M, D: Word;
  I: Integer;
begin
  DecodeDate(Date, Y, M, D);
  FCurrentYear := Y;
  FCurrentMonth := M;
  FLastHoverItemIndex := -1;
  FLastHoverDiaIndex := -1;
  FLastHoverMasIndex := -1;
  FSelectedOrdenId := 0;
  FSelectedFecha := Date;

  // Asegurar Tag = 999 para aislar controles de cambio
  cboMes.Tag := 999;
  cboAnio.Tag := 999;
  cbbObrador.Tag := 999;
  cbbEstado.Tag := 999;
  edtBuscar.Tag := 999;
  btnMesAnterior.Tag := 999;
  btnMesSiguiente.Tag := 999;
  btnHoy.Tag := 999;
  btnRefrescar.Tag := 999;
  btnNuevaOrden.Tag := 999;

  gridCalendario.DoubleBuffered := True;

  // Crear una única vez las etiquetas de cabecera de los días (sin Name para evitar cualquier duplicidad)
  for I := 0 to 6 do
  begin
    FLblDias[I] := TLabel.Create(Self);
    FLblDias[I].Parent := pnlDiasHeader;
    FLblDias[I].Font.Name := 'Segoe UI';
    FLblDias[I].Font.Size := 9;
    FLblDias[I].Font.Style := [fsBold];
    FLblDias[I].Font.Color := $00475569;
    FLblDias[I].Caption := NOMBRES_DIAS[I + 1];
    FLblDias[I].Alignment := taCenter;
    FLblDias[I].AutoSize := False;
    FLblDias[I].Tag := 999;
  end;

  InicializarFiltros;
end;

procedure TfrmCalendarioProduccion.FormShow(Sender: TObject);
begin
  AjustarTamanosGrid;
  CargarObradores;
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.FormResize(Sender: TObject);
begin
  AjustarTamanosGrid;
end;

procedure TfrmCalendarioProduccion.AjustarTamanosGrid;
var
  I: Integer;
  LColWidth, LRowHeight: Integer;
begin
  if gridCalendario.ClientWidth <= 0 then Exit;

  LColWidth := gridCalendario.ClientWidth div 7;
  for I := 0 to 6 do
    gridCalendario.ColWidths[I] := LColWidth;

  if gridCalendario.ClientHeight > 0 then
  begin
    LRowHeight := gridCalendario.ClientHeight div 6;
    for I := 0 to 5 do
      gridCalendario.RowHeights[I] := LRowHeight;
  end;

  // Posicionar etiquetas según el ancho de columna
  for I := 0 to 6 do
  begin
    if Assigned(FLblDias[I]) then
    begin
      FLblDias[I].Left := I * LColWidth;
      FLblDias[I].Top := 6;
      FLblDias[I].Width := LColWidth;
      FLblDias[I].Height := 18;
    end;
  end;
end;

procedure TfrmCalendarioProduccion.InicializarFiltros;
var
  I: Integer;
begin
  // Meses
  cboMes.Items.Clear;
  for I := 1 to 12 do
    cboMes.Items.Add(NOMBRES_MESES[I]);
  cboMes.ItemIndex := FCurrentMonth - 1;

  // Años
  cboAnio.Items.Clear;
  for I := FCurrentYear - 3 to FCurrentYear + 3 do
    cboAnio.Items.Add(IntToStr(I));
  cboAnio.ItemIndex := cboAnio.Items.IndexOf(IntToStr(FCurrentYear));

  // Estados
  cbbEstado.Items.Clear;
  cbbEstado.Items.Add('-- Todos los Estados --');
  cbbEstado.Items.Add('PLANIFICADA');
  cbbEstado.Items.Add('EN_PROCESO');
  cbbEstado.Items.Add('COMPLETADA');
  cbbEstado.Items.Add('CANCELADA');
  cbbEstado.ItemIndex := 0;
end;

procedure TfrmCalendarioProduccion.CargarObradores;
var
  LQry: TFDQuery;
begin
  cbbObrador.Items.Clear;
  cbbObrador.Items.AddObject('-- Todos los Obradores --', TObject(IntPtr(0)));

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT id, nombre FROM ge_obradores ORDER BY nombre';
    LQry.Open;
    while not LQry.Eof do
    begin
      cbbObrador.Items.AddObject(
        LQry.FieldByName('nombre').AsString,
        TObject(IntPtr(LQry.FieldByName('id').AsInteger))
      );
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;
  cbbObrador.ItemIndex := 0;
end;

function TfrmCalendarioProduccion.ObtenerFechaDeCelda(ACol, ARow: Integer): TDate;
var
  LIdx: Integer;
begin
  LIdx := ARow * 7 + ACol;
  if (LIdx >= 0) and (LIdx <= 41) then
    Result := FMatrizDias[LIdx].Fecha
  else
    Result := Date;
end;

procedure TfrmCalendarioProduccion.CargarDatosMes;
var
  LPrimerDiaMes: TDate;
  LDiaSemana: Integer;
  LInicioMatriz: TDate;
  LFinMatriz: TDate;
  I: Integer;
  LQry: TFDQuery;
  LEmpresaId, LObradorId: Integer;
  LFechaProd: TDate;
  LSql: string;
  LDiaIdx: Integer;
  LItem: TProduccionItem;
begin
  LEmpresaId := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  LPrimerDiaMes := EncodeDate(FCurrentYear, FCurrentMonth, 1);

  // DayOfTheWeek: 1 = Lunes .. 7 = Domingo
  LDiaSemana := DayOfTheWeek(LPrimerDiaMes);
  LInicioMatriz := LPrimerDiaMes - (LDiaSemana - 1);
  LFinMatriz := LInicioMatriz + 41;

  // 1. Inicializar la matriz de 42 días (6 semanas x 7 días)
  for I := 0 to 41 do
  begin
    FMatrizDias[I].Fecha := LInicioMatriz + I;
    FMatrizDias[I].EsMesActual := (MonthOf(FMatrizDias[I].Fecha) = FCurrentMonth);
    FMatrizDias[I].EsHoy := (FMatrizDias[I].Fecha = Date);
    FMatrizDias[I].TotalUnidades := 0;
    FMatrizDias[I].TieneMas := False;
    FMatrizDias[I].MasRect := Rect(0, 0, 0, 0);
    SetLength(FMatrizDias[I].Items, 0);
  end;

  // 2. Consulta SQL con datos de cabecera y líneas de órdenes
  LSql :=
    'SELECT op.id AS orden_id, op.numero_orden, op.fecha_produccion, op.estado AS orden_estado, ' +
    '       op.prioridad, op.id_obrador, o.nombre AS obrador_nombre, ' +
    '       op.id_pedido, p.numero_pedido, ' +
    '       opl.id AS linea_id, opl.id_articulo, a.descripcion AS articulo_nombre, ' +
    '       opl.cantidad, opl.cantidad_fabricada, opl.observaciones AS linea_observaciones, ' +
    '       opl.estado AS linea_estado ' +
    'FROM ge_ordenes_produccion op ' +
    'INNER JOIN ge_ordenes_produccion_lineas opl ON op.id = opl.id_orden ' +
    'LEFT JOIN ge_articulos a ON opl.id_articulo = a.id ' +
    'LEFT JOIN ge_obradores o ON op.id_obrador = o.id ' +
    'LEFT JOIN ge_pedidos p ON op.id_pedido = p.id ' +
    'WHERE op.empresa_id = :empresa_id ' +
    '  AND op.fecha_produccion BETWEEN :fecha_desde AND :fecha_hasta ';

  // Filtro de Obrador
  if cbbObrador.ItemIndex > 0 then
  begin
    LObradorId := Integer(IntPtr(cbbObrador.Items.Objects[cbbObrador.ItemIndex]));
    if LObradorId > 0 then
      LSql := LSql + 'AND op.id_obrador = :id_obrador ';
  end;

  // Filtro de Estado
  if cbbEstado.ItemIndex > 0 then
    LSql := LSql + 'AND op.estado = :estado ';

  // Filtro de Búsqueda
  if Trim(edtBuscar.Text) <> '' then
    LSql := LSql + 'AND (a.descripcion LIKE :search OR op.numero_orden LIKE :search OR opl.observaciones LIKE :search) ';

  LSql := LSql + 'ORDER BY op.fecha_produccion ASC, op.prioridad DESC, opl.id ASC';

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := LSql;
    LQry.ParamByName('empresa_id').AsInteger := LEmpresaId;
    LQry.ParamByName('fecha_desde').AsDate := LInicioMatriz;
    LQry.ParamByName('fecha_hasta').AsDate := LFinMatriz;

    if cbbObrador.ItemIndex > 0 then
    begin
      LObradorId := Integer(IntPtr(cbbObrador.Items.Objects[cbbObrador.ItemIndex]));
      if LObradorId > 0 then
        LQry.ParamByName('id_obrador').AsInteger := LObradorId;
    end;

    if cbbEstado.ItemIndex > 0 then
      LQry.ParamByName('estado').AsString := cbbEstado.Text;

    if Trim(edtBuscar.Text) <> '' then
      LQry.ParamByName('search').AsString := '%' + Trim(edtBuscar.Text) + '%';

    LQry.Open;

    while not LQry.Eof do
    begin
      LFechaProd := LQry.FieldByName('fecha_produccion').AsDateTime;
      LDiaIdx := DaysBetween(LFechaProd, LInicioMatriz);

      if (LDiaIdx >= 0) and (LDiaIdx <= 41) then
      begin
        LItem.OrdenId := LQry.FieldByName('orden_id').AsInteger;
        LItem.LineaId := LQry.FieldByName('linea_id').AsInteger;
        LItem.NumeroOrden := LQry.FieldByName('numero_orden').AsString;
        LItem.ArticuloId := LQry.FieldByName('id_articulo').AsInteger;
        LItem.ArticuloNombre := LQry.FieldByName('articulo_nombre').AsString;
        LItem.Cantidad := LQry.FieldByName('cantidad').AsFloat;
        LItem.CantidadFabricada := LQry.FieldByName('cantidad_fabricada').AsFloat;
        LItem.Estado := LQry.FieldByName('linea_estado').AsString;
        LItem.Prioridad := LQry.FieldByName('prioridad').AsString;
        LItem.Observaciones := LQry.FieldByName('linea_observaciones').AsString;
        LItem.PedidoNumero := LQry.FieldByName('numero_pedido').AsString;
        LItem.BoundsRect := Rect(0, 0, 0, 0);

        SetLength(FMatrizDias[LDiaIdx].Items, Length(FMatrizDias[LDiaIdx].Items) + 1);
        FMatrizDias[LDiaIdx].Items[High(FMatrizDias[LDiaIdx].Items)] := LItem;
        FMatrizDias[LDiaIdx].TotalUnidades := FMatrizDias[LDiaIdx].TotalUnidades + LItem.Cantidad;
      end;

      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  gridCalendario.Invalidate;
end;

procedure TfrmCalendarioProduccion.gridCalendarioDrawCell(Sender: TObject;
  ACol, ARow: Longint; Rect: TRect; State: TGridDrawState);
var
  LIdx: Integer;
  LDia: TDiaProduccion;
  LCanvas: TCanvas;
  LHeaderRect: TRect;
  LItemRect: TRect;
  LNumDiaStr: string;
  LBadgeStr: string;
  I: Integer;
  LCurrentTop: Integer;
  LItemText: string;
  LMaxItemsVisible: Integer;
  LColorBadge, LColorText: TColor;
begin
  LIdx := ARow * 7 + ACol;
  if (LIdx < 0) or (LIdx > 41) then Exit;

  LDia := FMatrizDias[LIdx];
  LCanvas := gridCalendario.Canvas;

  // 1. Fondo de Celda
  if not LDia.EsMesActual then
    LCanvas.Brush.Color := $00F6F6F6
  else if LDia.EsHoy then
    LCanvas.Brush.Color := $00FFFBF0
  else if gdSelected in State then
    LCanvas.Brush.Color := $00F0F7FF
  else
    LCanvas.Brush.Color := clWhite;

  LCanvas.FillRect(Rect);

  // Borde de Celda
  LCanvas.Pen.Color := $00E0E0E0;
  LCanvas.Pen.Width := 1;
  LCanvas.Rectangle(Rect);

  // 2. Cabecera del Día (Franja Superior de 24px)
  LHeaderRect := Rect;
  LHeaderRect.Bottom := LHeaderRect.Top + 24;

  if LDia.EsHoy then
  begin
    LCanvas.Brush.Color := $00E26714; // Azul corporativo
    LCanvas.FillRect(LHeaderRect);
    LCanvas.Font.Color := clWhite;
    LCanvas.Font.Style := [fsBold];
  end
  else
  begin
    if not LDia.EsMesActual then
      LCanvas.Font.Color := clGray
    else
      LCanvas.Font.Color := clBlack;
    LCanvas.Font.Style := [fsBold];
  end;

  LCanvas.Font.Name := 'Segoe UI';
  LCanvas.Font.Size := 10;
  LNumDiaStr := IntToStr(DayOf(LDia.Fecha));
  LCanvas.TextOut(LHeaderRect.Left + 6, LHeaderRect.Top + 3, LNumDiaStr);

  // Total de Unidades acumuladas del día
  if LDia.TotalUnidades > 0 then
  begin
    LBadgeStr := Format('%g uds', [LDia.TotalUnidades]);
    LCanvas.Font.Size := 8;
    if LDia.EsHoy then
      LCanvas.Font.Color := clWhite
    else
      LCanvas.Font.Color := $00855A2E;

    LCanvas.TextOut(
      LHeaderRect.Right - LCanvas.TextWidth(LBadgeStr) - 6,
      LHeaderRect.Top + 5,
      LBadgeStr
    );
  end;

  // 3. Renderizado de Artículos a Producir en el Día
  LCurrentTop := LHeaderRect.Bottom + 4;
  LMaxItemsVisible := (Rect.Bottom - LCurrentTop - 18) div 20;
  if LMaxItemsVisible < 1 then LMaxItemsVisible := 1;

  for I := 0 to High(LDia.Items) do
  begin
    if I >= LMaxItemsVisible then
    begin
      // Indicador interactivo de más artículos
      var LMasR: TRect;
      LMasR := TRect.Create(Rect.Left + 4, LCurrentTop, Rect.Right - 4, LCurrentTop + 18);
      FMatrizDias[LIdx].TieneMas := True;
      FMatrizDias[LIdx].MasRect := LMasR;

      LCanvas.Brush.Color := $00FEF3EB; // Fondo ámbar muy suave para indicar interactividad
      LCanvas.Pen.Color := $00FCD34D;
      LCanvas.RoundRect(LMasR.Left, LMasR.Top, LMasR.Right, LMasR.Bottom, 6, 6);

      LCanvas.Font.Size := 8;
      LCanvas.Font.Style := [fsBold, fsUnderline];
      LCanvas.Font.Color := $00B45309;
      DrawText(
        LCanvas.Handle,
        PChar(Format('+ %d producto(s) más...', [Length(LDia.Items) - I])),
        -1,
        LMasR,
        DT_CENTER or DT_VCENTER or DT_SINGLELINE
      );
      Break;
    end;

    LItemRect.Left := Rect.Left + 4;
    LItemRect.Top := LCurrentTop;
    LItemRect.Right := Rect.Right - 4;
    LItemRect.Bottom := LCurrentTop + 18;

    // Guardar coordenadas de la pastilla para Hit-Testing en MouseMove
    FMatrizDias[LIdx].Items[I].BoundsRect := LItemRect;

    // Colores según Estado de la Línea
    if LDia.Items[I].Estado = 'COMPLETADA' then
    begin
      LColorBadge := $00EAF7EC;
      LColorText := $001B6B22;
    end
    else if LDia.Items[I].Estado = 'EN_PROCESO' then
    begin
      LColorBadge := $00FEF3EB;
      LColorText := $00C06000;
    end
    else
    begin
      LColorBadge := $00F1F5F9;
      LColorText := $00334155;
    end;

    // Pastilla redondeada del artículo
    LCanvas.Brush.Color := LColorBadge;
    LCanvas.Pen.Color := LColorBadge;
    LCanvas.RoundRect(LItemRect.Left, LItemRect.Top, LItemRect.Right, LItemRect.Bottom, 6, 6);

    // Texto: [Cantidad]x [Nombre Artículo]
    LCanvas.Font.Size := 8;
    LCanvas.Font.Style := [fsBold];
    LCanvas.Font.Color := LColorText;
    LItemText := Format('%gx %s', [LDia.Items[I].Cantidad, LDia.Items[I].ArticuloNombre]);

    // Truncar con elipsis si excede el ancho
    DrawText(
      LCanvas.Handle,
      PChar(LItemText),
      Length(LItemText),
      LItemRect,
      DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS
    );

    Inc(LCurrentTop, 20);
  end;
end;

function TfrmCalendarioProduccion.ItemBajoCursor(X, Y: Integer;
  var ADiaIdx, AItemIdx: Integer): Boolean;
var
  LCol, LRow: Integer;
  LIdx, I: Integer;
begin
  Result := False;
  ADiaIdx := -1;
  AItemIdx := -1;

  gridCalendario.MouseToCell(X, Y, LCol, LRow);
  if (LCol < 0) or (LRow < 0) or (LCol > 6) or (LRow > 5) then Exit;

  LIdx := LRow * 7 + LCol;
  if (LIdx < 0) or (LIdx > 41) then Exit;

  for I := 0 to High(FMatrizDias[LIdx].Items) do
  begin
    if PtInRect(FMatrizDias[LIdx].Items[I].BoundsRect, Point(X, Y)) then
    begin
      ADiaIdx := LIdx;
      AItemIdx := I;
      Result := True;
      Exit;
    end;
  end;
end;

function TfrmCalendarioProduccion.MasBajoCursor(X, Y: Integer; var ADiaIdx: Integer): Boolean;
var
  LCol, LRow, LIdx: Integer;
begin
  Result := False;
  ADiaIdx := -1;

  gridCalendario.MouseToCell(X, Y, LCol, LRow);
  if (LCol < 0) or (LRow < 0) or (LCol > 6) or (LRow > 5) then Exit;

  LIdx := LRow * 7 + LCol;
  if (LIdx < 0) or (LIdx > 41) then Exit;

  if FMatrizDias[LIdx].TieneMas and PtInRect(FMatrizDias[LIdx].MasRect, Point(X, Y)) then
  begin
    ADiaIdx := LIdx;
    Result := True;
  end;
end;

function TfrmCalendarioProduccion.ConstruirHintMasArticulos(ADiaIdx: Integer): string;
var
  LDia: TDiaProduccion;
  I: Integer;
  LFechaStr: string;
  LSb: TStringBuilder;
  LItem: TProduccionItem;
begin
  Result := '';
  if (ADiaIdx < 0) or (ADiaIdx > 41) then Exit;
  LDia := FMatrizDias[ADiaIdx];

  LFechaStr := FormatDateTime('dddd, dd/MM/yyyy', LDia.Fecha);
  if Length(LFechaStr) > 0 then
    LFechaStr[1] := UpCase(LFechaStr[1]);

  LSb := TStringBuilder.Create;
  try
    LSb.AppendFormat('📅 %s  •  TOTAL: %g uds'#13#10, [LFechaStr, LDia.TotalUnidades]);
    LSb.AppendLine('------------------------------------------------------------');
    LSb.AppendFormat('📦 TODOS LOS PRODUCTOS PROGRAMADOS (%d):'#13#10, [Length(LDia.Items)]);

    for I := 0 to High(LDia.Items) do
    begin
      LItem := LDia.Items[I];
      LSb.AppendFormat(' • %gx %s [%s]'#13#10,
        [LItem.Cantidad, LItem.ArticuloNombre, LItem.NumeroOrden]);
    end;

    LSb.AppendLine('------------------------------------------------------------');
    LSb.Append('👉 Haga clic aquí para abrir la lista completa y editar cualquier orden.');
    Result := LSb.ToString;
  finally
    LSb.Free;
  end;
end;

procedure TfrmCalendarioProduccion.gridCalendarioMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
var
  LDiaIdx, LItemIdx: Integer;
  LItem: TProduccionItem;
  LHintText: string;
  LObsText: string;
begin
  // 1. Detección sobre botón interactivo "+ N producto(s) más..."
  if MasBajoCursor(X, Y, LDiaIdx) then
  begin
    gridCalendario.Cursor := crHandPoint;
    if LDiaIdx <> FLastHoverMasIndex then
    begin
      FLastHoverMasIndex := LDiaIdx;
      FLastHoverDiaIndex := -1;
      FLastHoverItemIndex := -1;
      gridCalendario.Hint := ConstruirHintMasArticulos(LDiaIdx);
      Application.ActivateHint(Mouse.CursorPos);
    end;
    Exit;
  end;

  FLastHoverMasIndex := -1;

  // 2. Detección sobre pastilla de artículo individual
  if ItemBajoCursor(X, Y, LDiaIdx, LItemIdx) then
  begin
    gridCalendario.Cursor := crHandPoint;

    if (LDiaIdx <> FLastHoverDiaIndex) or (LItemIdx <> FLastHoverItemIndex) then
    begin
      FLastHoverDiaIndex := LDiaIdx;
      FLastHoverItemIndex := LItemIdx;
      LItem := FMatrizDias[LDiaIdx].Items[LItemIdx];

      LObsText := Trim(LItem.Observaciones);
      if LObsText = '' then
        LObsText := '(Sin observaciones específicas de línea)';

      LHintText :=
        Format(
          '🥐 ARTÍCULO: %s'#13#10 +
          '📦 Cantidad Requerida: %g uds  |  Fabricada: %g uds'#13#10 +
          '🏷️ Estado: %s  |  Prioridad: %s'#13#10 +
          '📋 Orden: %s%s'#13#10 +
          '------------------------------------------------------------'#13#10 +
          '📝 OBSERVACIONES DE LÍNEA:'#13#10 +
          '%s',
          [
            LItem.ArticuloNombre,
            LItem.Cantidad,
            LItem.CantidadFabricada,
            LItem.Estado,
            LItem.Prioridad,
            LItem.NumeroOrden,
            IfThen(LItem.PedidoNumero <> '', ' (Pedido: ' + LItem.PedidoNumero + ')', ''),
            LObsText
          ]
        );

      gridCalendario.Hint := LHintText;
      Application.ActivateHint(Mouse.CursorPos);
    end;
  end
  else
  begin
    gridCalendario.Cursor := crDefault;
    FLastHoverDiaIndex := -1;
    FLastHoverItemIndex := -1;
    gridCalendario.Hint := '';
  end;
end;

function TfrmCalendarioProduccion.ObtenerOrdenDeCelda(ACol, ARow, X, Y: Integer): Integer;
var
  LCellDiaIdx, LFoundDia, LFoundItem: Integer;
begin
  Result := 0;
  if (ACol < 0) or (ARow < 0) or (ACol > 6) or (ARow > 5) then Exit;
  LCellDiaIdx := ARow * 7 + ACol;
  if (LCellDiaIdx < 0) or (LCellDiaIdx > 41) then Exit;

  // Solo devuelve la orden si el cursor está directamente sobre una pastilla/línea concreta
  if ItemBajoCursor(X, Y, LFoundDia, LFoundItem) then
  begin
    if (LFoundDia >= 0) and (LFoundDia <= 41) and
       (LFoundItem >= 0) and (LFoundItem < Length(FMatrizDias[LFoundDia].Items)) then
    begin
      Result := FMatrizDias[LFoundDia].Items[LFoundItem].OrdenId;
    end;
  end;
end;

procedure TfrmCalendarioProduccion.gridCalendarioMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  LCol, LRow, LDiaIdx: Integer;
begin
  gridCalendario.MouseToCell(X, Y, LCol, LRow);
  if (LCol >= 0) and (LRow >= 0) then
  begin
    FSelectedFecha := ObtenerFechaDeCelda(LCol, LRow);
    FSelectedOrdenId := ObtenerOrdenDeCelda(LCol, LRow, X, Y);

    // Si el usuario hace clic izquierdo sobre "+ N producto(s) más...":
    if (Button = mbLeft) and MasBajoCursor(X, Y, LDiaIdx) then
    begin
      AbrirDetalleDia(FSelectedFecha);
      Exit;
    end;
  end
  else
    FSelectedOrdenId := 0;
end;

procedure TfrmCalendarioProduccion.gridCalendarioDblClick(Sender: TObject);
var
  LEditor: TfrmOrdenProduccionEditor;
  LMousePos: TPoint;
  LCol, LRow, LDiaIdx: Integer;
begin
  if Sender <> nil then
  begin
    LMousePos := gridCalendario.ScreenToClient(Mouse.CursorPos);
    gridCalendario.MouseToCell(LMousePos.X, LMousePos.Y, LCol, LRow);
    if (LCol >= 0) and (LRow >= 0) then
    begin
      FSelectedFecha := ObtenerFechaDeCelda(LCol, LRow);
      FSelectedOrdenId := ObtenerOrdenDeCelda(LCol, LRow, LMousePos.X, LMousePos.Y);

      // Si hace doble clic sobre "+ N más..." o en un día que tiene artículos y no seleccionó una línea concreta:
      LDiaIdx := LRow * 7 + LCol;
      if (LDiaIdx >= 0) and (LDiaIdx <= 41) then
      begin
        if MasBajoCursor(LMousePos.X, LMousePos.Y, LDiaIdx) or
           ((FSelectedOrdenId <= 0) and (Length(FMatrizDias[LDiaIdx].Items) > 0)) then
        begin
          AbrirDetalleDia(FSelectedFecha);
          Exit;
        end;
      end;
    end
    else
      FSelectedOrdenId := 0;
  end;

  if FSelectedOrdenId <= 0 then
  begin
    ShowMessage('No hay ninguna orden de producción seleccionada.' + sLineBreak +
                'Haga doble clic directamente sobre una línea de orden para abrirla.');
    Exit;
  end;

  LEditor := TfrmOrdenProduccionEditor.Create(Application);
  try
    LEditor.OrdenId := FSelectedOrdenId;
    LEditor.InitialFechaProduccion := FSelectedFecha;
    if LEditor.ShowModal = mrOk then
      CargarDatosMes;
  finally
    LEditor.Free;
  end;
end;

procedure TfrmCalendarioProduccion.btnMesAnteriorClick(Sender: TObject);
begin
  if FCurrentMonth = 1 then
  begin
    FCurrentMonth := 12;
    Dec(FCurrentYear);
  end
  else
    Dec(FCurrentMonth);

  cboMes.ItemIndex := FCurrentMonth - 1;
  cboAnio.ItemIndex := cboAnio.Items.IndexOf(IntToStr(FCurrentYear));
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.btnMesSiguienteClick(Sender: TObject);
begin
  if FCurrentMonth = 12 then
  begin
    FCurrentMonth := 1;
    Inc(FCurrentYear);
  end
  else
    Inc(FCurrentMonth);

  cboMes.ItemIndex := FCurrentMonth - 1;
  cboAnio.ItemIndex := cboAnio.Items.IndexOf(IntToStr(FCurrentYear));
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.btnHoyClick(Sender: TObject);
var
  Y, M, D: Word;
begin
  DecodeDate(Date, Y, M, D);
  FCurrentYear := Y;
  FCurrentMonth := M;
  cboMes.ItemIndex := FCurrentMonth - 1;
  cboAnio.ItemIndex := cboAnio.Items.IndexOf(IntToStr(FCurrentYear));
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.cboPeriodoChange(Sender: TObject);
begin
  FCurrentMonth := cboMes.ItemIndex + 1;
  FCurrentYear := StrToIntDef(cboAnio.Text, CurrentYear);
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.cbbFiltroChange(Sender: TObject);
begin
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.edtBuscarKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key = VK_RETURN) or (Trim(edtBuscar.Text) = '') then
    CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.btnRefrescarClick(Sender: TObject);
begin
  CargarDatosMes;
end;

procedure TfrmCalendarioProduccion.btnNuevaOrdenClick(Sender: TObject);
var
  LEditor: TfrmOrdenProduccionEditor;
begin
  LEditor := TfrmOrdenProduccionEditor.Create(Application);
  try
    LEditor.OrdenId := 0;
    LEditor.InitialFechaProduccion := FSelectedFecha;
    if LEditor.ShowModal = mrOk then
      CargarDatosMes;
  finally
    LEditor.Free;
  end;
end;

procedure TfrmCalendarioProduccion.miEditarOrdenClick(Sender: TObject);
begin
  if FSelectedOrdenId > 0 then
    gridCalendarioDblClick(nil)
  else
    ShowMessage('No hay ninguna orden de producción seleccionada.' + sLineBreak +
                'Haga clic derecho sobre una orden para editarla o use "Nueva Orden".');
end;

procedure TfrmCalendarioProduccion.miEliminarOrdenClick(Sender: TObject);
var
  LQry: TFDQuery;
begin
  if FSelectedOrdenId <= 0 then
  begin
    ShowMessage('Seleccione una orden para eliminar.');
    Exit;
  end;

  if MessageDlg('¿Está seguro de que desea eliminar la orden de producción seleccionada?',
                mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'DELETE FROM ge_ordenes_produccion WHERE id = :id';
      LQry.ParamByName('id').AsInteger := FSelectedOrdenId;
      LQry.ExecSQL;
      ShowMessage('Orden de producción eliminada.');
      CargarDatosMes;
    finally
      LQry.Free;
    end;
  end;
end;

procedure TfrmCalendarioProduccion.miVerDetalleDiaClick(Sender: TObject);
begin
  if FSelectedFecha > 0 then
    AbrirDetalleDia(FSelectedFecha);
end;

procedure TfrmCalendarioProduccion.AbrirDetalleDia(AFecha: TDate);
var
  LDlg: TfrmDiaDetalleProduccion;
  LEmpresaId: Integer;
begin
  if AFecha <= 0 then Exit;

  LEmpresaId := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  LDlg := TfrmDiaDetalleProduccion.Create(Application);
  try
    LDlg.Fecha := AFecha;
    LDlg.EmpresaId := LEmpresaId;
    LDlg.ShowModal;
    CargarDatosMes;
  finally
    LDlg.Free;
  end;
end;

end.
