unit View.TasksHTML;

interface

uses
  System.SysUtils, System.Classes,
  JS, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
  WEBLib.ExtCtrls,
  XData.Web.Client, XData.Web.Dataset,
  Utils, Data.DB, XData.Web.JsonDataset, Vcl.Controls, Vcl.StdCtrls,
  WEBLib.StdCtrls;

type
  TFTasksHTML = class(TWebForm)
    xdwcTasks: TXDataWebClient;
    xdwdsTasks: TXDataWebDataSet;
    xdwdsTaskstaskId: TStringField;
    xdwdsTasksapplication: TStringField;
    xdwdsTasksversion: TStringField;
    xdwdsTaskstaskDate: TStringField;
    xdwdsTasksreportedBy: TStringField;
    xdwdsTasksassignedTo: TStringField;
    xdwdsTasksstatus: TStringField;
    xdwdsTasksstatusDate: TStringField;
    xdwdsTasksformSection: TStringField;
    xdwdsTasksissue: TStringField;
    xdwdsTasksnotes: TStringField;
    btnReload: TWebButton;
    btnAddRow: TWebButton;
    xdwdsTaskstaskItemId: TStringField;
    procedure btnAddRowClick(Sender: TObject);
    procedure btnReloadClick(Sender: TObject);
    procedure WebFormCreate(Sender: TObject);
  private
    [async] procedure LoadTasks(const AProjectId: string);
    procedure RenderTable;

    procedure BindTableEditors;
    procedure EditorInput(Event: TJSEvent);
    procedure SelectChange(Event: TJSEvent);

    procedure EnableColumnResize;
    procedure EnableAutoGrowTextAreas;

    procedure GotoRowIndex(AIndex: Integer);
    function HtmlEncode(const s: string): string;
    procedure SetProjectLabel(const AProjectId: string);

    [async] procedure SaveRow(AIndex: Integer);
    procedure EditorBlur(Event: TJSEvent);
  public
  end;

var
  FTasksHTML: TFTasksHTML;

implementation

uses
  ConnectionModule;

{$R *.dfm}

procedure TFTasksHTML.WebFormCreate(Sender: TObject);
begin
  LoadTasks('WPR0001');
end;


function TFTasksHTML.HtmlEncode(const s: string): string;
begin
  Result := s;
  Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
  Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;

procedure TFTasksHTML.GotoRowIndex(AIndex: Integer);
var
  i: Integer;
begin
  if (AIndex < 0) or (not xdwdsTasks.Active) then
    Exit;

  xdwdsTasks.First;
  i := 0;
  while (i < AIndex) and (not xdwdsTasks.Eof) do
  begin
    xdwdsTasks.Next;
    Inc(i);
  end;
end;

procedure TFTasksHTML.EditorInput(Event: TJSEvent);
var
  el: TJSHTMLElement;
  idx: Integer;
  idxStr, fieldName: string;
  newVal: string;
begin
  if not xdwdsTasks.Active then
    Exit;

  el := TJSHTMLElement(Event.target);

  idxStr := string(el.getAttribute('data-idx'));
  fieldName := string(el.getAttribute('data-field'));
  idx := StrToIntDef(idxStr, -1);
  if (idx < 0) or (fieldName = '') then
    Exit;

  GotoRowIndex(idx);
  if xdwdsTasks.Eof then
    Exit;

  newVal := string(TJSObject(el)['value']);

  console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal);

  xdwdsTasks.Edit;
  xdwdsTasks.FieldByName(fieldName).AsString := newVal;
  xdwdsTasks.Post;

  el.setAttribute('data-unsaved-data', '1');
end;



procedure TFTasksHTML.SelectChange(Event: TJSEvent);
var
  el: TJSHTMLElement;
  idx: Integer;
  idxStr, fieldName: string;
  sel: TJSHTMLSelectElement;
begin
  if not xdwdsTasks.Active then
    Exit;

  el := TJSHTMLElement(Event.target);

  idxStr := string(el.getAttribute('data-idx'));
  fieldName := string(el.getAttribute('data-field'));
  idx := StrToIntDef(idxStr, -1);
  if (idx < 0) or (fieldName = '') then
    Exit;

  GotoRowIndex(idx);
  if xdwdsTasks.Eof then
    Exit;

  sel := TJSHTMLSelectElement(el);

  console.log('SelectChange: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + string(sel.value));

  xdwdsTasks.Edit;
  xdwdsTasks.FieldByName(fieldName).AsString := string(sel.value);
  xdwdsTasks.Post;

  el.setAttribute('data-unsaved-data', '1');
  SaveRow(idx);
end;


procedure TFTasksHTML.BindTableEditors;
var
  nodes: TJSNodeList;
  i: Integer;
  el: TJSHTMLElement;
begin
  console.log('BindTableEditors: wiring handlers...');

  nodes := document.querySelectorAll('.task-editor');
  console.log('BindTableEditors: task-editor count=' + IntToStr(nodes.length));
  for i := 0 to nodes.length - 1 do
  begin
    el := TJSHTMLElement(nodes.item(i));
    el.addEventListener('input', TJSEventHandler(@EditorInput));
    el.addEventListener('blur', TJSEventHandler(@EditorBlur));
  end;

  nodes := document.querySelectorAll('.task-select');
  console.log('BindTableEditors: task-select count=' + IntToStr(nodes.length));
  for i := 0 to nodes.length - 1 do
  begin
    el := TJSHTMLElement(nodes.item(i));
    el.addEventListener('change', TJSEventHandler(@SelectChange));
  end;
end;

procedure TFTasksHTML.btnAddRowClick(Sender: TObject);
begin
  if not xdwdsTasks.Active then
    Exit;

  xdwdsTasks.Append;
  xdwdsTaskstaskId.AsString := 'NEW_TASK_ID';
  xdwdsTasksreportedBy.AsString := '';
  xdwdsTasksassignedTo.AsString := '';
  xdwdsTasksstatus.AsString := '';
  xdwdsTasks.Post;

  RenderTable;
end;


procedure TFTasksHTML.btnReloadClick(Sender: TObject);
begin
  LoadTasks('WPR0001');
end;

procedure TFTasksHTML.EnableAutoGrowTextAreas;
begin
  asm
    (function(){
      const host = document.getElementById('tasks_table_host');
      if(!host) return;
      host.querySelectorAll('textarea.cell-textarea').forEach(ta => {
        const fit = () => { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; };
        fit();
        ta.addEventListener('input', fit);
      });
    })();
  end;
end;

procedure TFTasksHTML.SetProjectLabel(const AProjectId: string);
var
  el: TJSHTMLElement;
begin
  el := TJSHTMLElement(document.getElementById('lbl_project_name'));
  if Assigned(el) then
    el.innerText := 'Tasks - ' + AProjectId;
end;


[async] procedure TFTasksHTML.LoadTasks(const AProjectId: string);
var
  response: TXDataClientResponse;
  resultObj, taskObj: TJSObject;
  tasksArray, itemsArray, flatItems: TJSArray;
  taskIndex, itemIndex: Integer;
begin
  SetProjectLabel(AProjectId);
  Utils.ShowSpinner('spinner');
  try
    response := await(xdwcTasks.RawInvokeAsync(
      'IApiService.GetProjectTasks', [AProjectId]
    ));

    if not Assigned(response.Result) then
      Exit;

    resultObj := TJSObject(response.Result);
    tasksArray := TJSArray(resultObj['data']);
    flatItems := TJSArray.new;

    for taskIndex := 0 to tasksArray.Length - 1 do
    begin
      taskObj := TJSObject(tasksArray[taskIndex]);
      itemsArray := TJSArray(taskObj['items']);

      for itemIndex := 0 to itemsArray.Length - 1 do
        flatItems.push(itemsArray[itemIndex]);
    end;

    xdwdsTasks.Close;
    xdwdsTasks.SetJsonData(flatItems);
    xdwdsTasks.Open;

    RenderTable;
  finally
    Utils.HideSpinner('spinner');
  end;
end;

procedure TFTasksHTML.RenderTable;
var
  host: TJSHTMLElement;
  html: string;
  rowIdx: Integer;

  function Th(const s: string): string;
  begin
    Result := '<th scope="col">' + s + '</th>';
  end;

  function TdNowrap(const s: string): string;
  begin
    Result := '<td class="align-top nowrap-cell">' + s + '</td>';
  end;

  function TdWrap(const s: string): string;
  begin
    Result := '<td class="align-top wrap-cell">' + s + '</td>';
  end;

  function TextInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
  var
    w: string;
  begin
    w := '';
    if MinWidth > 0 then
      w := ' style="min-width: ' + IntToStr(MinWidth) + 'px;"';

    Result :=
      '<input class="form-control form-control-sm cell-input task-editor w-100" ' +
        'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
        'value="' + HtmlEncode(Value) + '"' + w + '>';
  end;

  function TextArea(const FieldName, Value: string; const AIdx: Integer): string;
  begin
    Result :=
      '<textarea class="form-control form-control-sm cell-textarea task-editor w-100" ' +
        'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
        'rows="2">' + HtmlEncode(Value) + '</textarea>';
  end;

  function DateInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
  var
    w: string;
  begin
    w := '';
    if MinWidth > 0 then
      w := ' style="min-width: ' + IntToStr(MinWidth) + 'px;"';

    Result :=
      '<input type="date" class="form-control form-control-sm cell-input task-editor w-100" ' +
        'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
        'value="' + HtmlEncode(Value) + '"' + w + '>';
  end;

  function SelectList(const FieldName, Current: string; const AIdx: Integer; const Items: array of string): string;
  var
    i: Integer;
    sel: string;
  begin
    Result :=
      '<select class="form-select form-select-sm task-select" ' +
        'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '">';

    sel := '';
    if Trim(Current) = '' then
      sel := ' selected';
    Result := Result + '<option value=""' + sel + '></option>';

    for i := Low(Items) to High(Items) do
    begin
      sel := '';
      if SameText(Current, Items[i]) then
        sel := ' selected';
      Result := Result + '<option value="' + HtmlEncode(Items[i]) + '"' + sel + '>' + HtmlEncode(Items[i]) + '</option>';
    end;

    Result := Result + '</select>';
  end;

  function StatusSelect(const Current: string; const AIdx: Integer): string;
  const
    Statuses: array[0..5] of string = ('Open', 'In Progress', 'Blocked', 'Testing', 'Done', 'Closed');
  var
    i: Integer;
    sel: string;
  begin
    Result :=
      '<select class="form-select form-select-sm task-select" data-idx="' + IntToStr(AIdx) + '" data-field="status">';

    for i := Low(Statuses) to High(Statuses) do
    begin
      sel := '';
      if SameText(Current, Statuses[i]) then
        sel := ' selected';
      Result := Result + '<option value="' + HtmlEncode(Statuses[i]) + '"' + sel + '>' + HtmlEncode(Statuses[i]) + '</option>';
    end;

    Result := Result + '</select>';
  end;

begin
  host := TJSHTMLElement(document.getElementById('tasks_table_host'));
  if not Assigned(host) then
    Exit;

  html :=
    '<div class="tasks-vscroll">' +
      '<div class="tasks-hscroll">' +
        '<table class="table table-sm table-bordered table-hover align-middle mb-0">' +
          '<colgroup>' +
            '<col style="width:110px">' +
            '<col style="width:240px">' +
            '<col style="width:90px">' +
            '<col style="width:120px">' +
            '<col style="width:120px">' +
            '<col style="width:120px">' +
            '<col style="width:140px">' +
            '<col style="width:140px">' +
            '<col style="width:160px">' +
            '<col style="width:520px">' +
            '<col style="width:520px">' +
          '</colgroup>' +
          '<thead><tr>' +
            Th('Task') +
            Th('App') +
            Th('Version') +
            Th('Date') +
            Th('Reported') +
            Th('Assigned') +
            Th('Status') +
            Th('Status Date') +
            Th('Form') +
            Th('Issue') +
            Th('Notes') +
          '</tr></thead><tbody>';

  rowIdx := 0;
  xdwdsTasks.First;
  while not xdwdsTasks.Eof do
  begin
    html := html +
      '<tr>' +
        TdNowrap(TextInput('taskId', xdwdsTaskstaskId.AsString, rowIdx, 90)) +
        TdNowrap(TextInput('application', xdwdsTasksapplication.AsString, rowIdx, 180)) +
        TdNowrap(TextInput('version', xdwdsTasksversion.AsString, rowIdx, 80)) +
        TdNowrap(DateInput('taskDate', xdwdsTaskstaskDate.AsString, rowIdx, 110)) +
        TdNowrap(SelectList('reportedBy', xdwdsTasksreportedBy.AsString, rowIdx, ['Elias','Mac','Mark'])) +
        TdNowrap(SelectList('assignedTo', xdwdsTasksassignedTo.AsString, rowIdx, ['Elias','Mac','Mark'])) +
        TdNowrap(StatusSelect(xdwdsTasksstatus.AsString, rowIdx)) +
        TdNowrap(DateInput('statusDate', xdwdsTasksstatusDate.AsString, rowIdx, 110)) +
        TdNowrap(TextInput('formSection', xdwdsTasksformSection.AsString, rowIdx, 160)) +
        TdWrap(TextArea('issue', xdwdsTasksissue.AsString, rowIdx)) +
        TdWrap(TextArea('notes', xdwdsTasksnotes.AsString, rowIdx)) +
      '</tr>';

    xdwdsTasks.Next;
    Inc(rowIdx);
  end;

  html := html + '</tbody></table></div></div>';
  host.innerHTML := html;

  BindTableEditors;
  EnableAutoGrowTextAreas;
  EnableColumnResize;
end;

procedure TFTasksHTML.EnableColumnResize;
begin
  asm
    (function(){
      const host = document.getElementById('tasks_table_host');
      if(!host) return;
      const table = host.querySelector('table');
      if(!table) return;
      const ths = table.querySelectorAll('thead th');
      ths.forEach(th => {
        th.classList.add('th-resize');
        if(th.querySelector('.th-resize-handle')) return;

        const handle = document.createElement('div');
        handle.className = 'th-resize-handle';
        th.appendChild(handle);

        handle.addEventListener('mousedown', function(e){
          e.preventDefault();
          const startX = e.clientX;
          const startW = th.getBoundingClientRect().width;

          function onMove(ev){
            const w = Math.max(40, startW + (ev.clientX - startX));
            th.style.width = w + 'px';
          }
          function onUp(){
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onUp);
          }
          document.addEventListener('mousemove', onMove);
          document.addEventListener('mouseup', onUp);
        });
      });
    })();
  end;
end;


procedure TFTasksHTML.EditorBlur(Event: TJSEvent);
var
  el: TJSHTMLElement;
  idx: Integer;
  idxStr, fieldName: string;
begin
  el := TJSHTMLElement(Event.target);

  idxStr := string(el.getAttribute('data-idx'));
  fieldName := string(el.getAttribute('data-field'));

  if string(el.getAttribute('data-unsaved-data')) <> '1' then
  begin
    console.log('EditorBlur: skip (not unsaved) idx=' + idxStr + ' field=' + fieldName);
    Exit;
  end;

  el.removeAttribute('data-unsaved-data');

  idx := StrToIntDef(idxStr, -1);
  if idx < 0 then
    Exit;

  console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);

  SaveRow(idx);
end;


[async] procedure TFTasksHTML.SaveRow(AIndex: Integer);
const
  // Note: Use this to manipulate saving to the server or not for testing
  ENABLE_SERVER_SAVE = True;
var
  response: TXDataClientResponse;
  payload: TJSObject;
  payloadJson: string;
begin
  if not xdwdsTasks.Active then
    Exit;

  GotoRowIndex(AIndex);
  if xdwdsTasks.Eof then
    Exit;

  payload := TJSObject.new;

  payload['taskItemId'] := xdwdsTaskstaskItemId.AsString;
  payload['taskId'] := xdwdsTaskstaskId.AsString;

  payload['application'] := xdwdsTasksapplication.AsString;
  payload['version'] := xdwdsTasksversion.AsString;
  payload['taskDate'] := xdwdsTaskstaskDate.AsString;
  payload['reportedBy'] := xdwdsTasksreportedBy.AsString;
  payload['assignedTo'] := xdwdsTasksassignedTo.AsString;
  payload['status'] := xdwdsTasksstatus.AsString;
  payload['statusDate'] := xdwdsTasksstatusDate.AsString;
  payload['formSection'] := xdwdsTasksformSection.AsString;
  payload['issue'] := xdwdsTasksissue.AsString;
  payload['notes'] := xdwdsTasksnotes.AsString;

  payloadJson := string(TJSJSON.stringify(payload));
  console.log('SaveRow: idx=' + IntToStr(AIndex) + ' payload=' + payloadJson);

  if not ENABLE_SERVER_SAVE then
    Exit;

  try
    response := await(xdwcTasks.RawInvokeAsync('IApiService.SaveTaskRow', [payload]));
    console.log('SaveRow: response=' + string(TJSJSON.stringify(response.Result)));
  except
    on E: EXDataClientRequestException do
    begin
      console.log('SaveRow ERROR: ' + E.ErrorResult.ErrorMessage);
      Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
    end;
  end;
end;




end.

