Commit 29263ec6 by Mac Stephens

add row validation, saving

parent f6aacf29
...@@ -3,6 +3,16 @@ object FTimeEntries: TFTimeEntries ...@@ -3,6 +3,16 @@ object FTimeEntries: TFTimeEntries
Height = 480 Height = 480
ElementFont = efCSS ElementFont = efCSS
OnCreate = WebFormCreate OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel
Left = 90
Top = 32
Width = 69
Height = 15
ElementID = 'lbl_validation_message'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object edtWeekOf: TWebEdit object edtWeekOf: TWebEdit
Left = 71 Left = 71
Top = 80 Top = 80
......
...@@ -23,5 +23,6 @@ ...@@ -23,5 +23,6 @@
<button id="btn_add_entry" class="btn btn-sm btn-success text-nowrap">Add Entry</button> <button id="btn_add_entry" class="btn btn-sm btn-success text-nowrap">Add Entry</button>
</div> </div>
</div> </div>
<div id="lbl_validation_message" class="alert alert-danger py-1 px-2 mb-2 d-none small"></div>
<div id="time_entries_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div> <div id="time_entries_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div>
</div> </div>
...@@ -24,6 +24,7 @@ type ...@@ -24,6 +24,7 @@ type
xdwdsTimeEntriessummary: TStringField; xdwdsTimeEntriessummary: TStringField;
xdwdsTimeEntriescategoryDesc: TStringField; xdwdsTimeEntriescategoryDesc: TStringField;
btnAddEntry: TWebButton; btnAddEntry: TWebButton;
lblValidationMessage: TWebLabel;
procedure edtWeekOfChange(Sender: TObject); procedure edtWeekOfChange(Sender: TObject);
procedure edtStartDateChange(Sender: TObject); procedure edtStartDateChange(Sender: TObject);
procedure edtEndDateChange(Sender: TObject); procedure edtEndDateChange(Sender: TObject);
...@@ -39,6 +40,10 @@ type ...@@ -39,6 +40,10 @@ type
FUpdatingDates: Boolean; FUpdatingDates: Boolean;
FPendingScrollTop: Integer; FPendingScrollTop: Integer;
FPendingScrollLeft: Integer; FPendingScrollLeft: Integer;
FActiveRowIndex: Integer;
FPendingEntryId: string;
FLastMouseDownRowIndex: Integer;
FLastMouseDownTime: Double;
[async] procedure LoadTimeEntries; [async] procedure LoadTimeEntries;
[async] function AddTimeEntry: Boolean; [async] function AddTimeEntry: Boolean;
procedure RenderTable; procedure RenderTable;
...@@ -55,6 +60,19 @@ type ...@@ -55,6 +60,19 @@ type
procedure SetTimeEntriesLabel(const AName: string); procedure SetTimeEntriesLabel(const AName: string);
procedure CaptureTableScroll; procedure CaptureTableScroll;
procedure RestoreTableScroll; procedure RestoreTableScroll;
procedure EditorInput(Event: TJSEvent);
procedure RowFocusOut(Event: TJSEvent);
procedure RowClick(Event: TJSEvent);
function ValidateRow(AIndex: Integer): Boolean;
procedure ApplyPendingEntryFocus;
procedure ApplyRowValidation(AIndex: Integer);
procedure ClearRowValidation(AIndex: Integer);
procedure ShowRowValidationMessage(AIndex: Integer; const AMessage: string);
procedure HideRowValidationMessage;
[async] procedure SaveRow(AIndex: Integer);
procedure SetTopControlsEnabled(AEnabled: Boolean);
function GetTargetRowIndex(ATarget: TJSHTMLElement): Integer;
procedure DocumentMouseDown(Event: TJSEvent);
public public
end; end;
...@@ -81,6 +99,12 @@ begin ...@@ -81,6 +99,12 @@ begin
FUpdatingDates := False; FUpdatingDates := False;
FPendingScrollTop := 0; FPendingScrollTop := 0;
FPendingScrollLeft := 0; FPendingScrollLeft := 0;
FActiveRowIndex := -1;
FPendingEntryId := '';
FLastMouseDownRowIndex := -1;
FLastMouseDownTime := 0;
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
payload := AuthService.TokenPayload; payload := AuthService.TokenPayload;
if Assigned(payload) then if Assigned(payload) then
...@@ -113,6 +137,7 @@ begin ...@@ -113,6 +137,7 @@ begin
LoadTimeEntries; LoadTimeEntries;
end; end;
function TFTimeEntries.HtmlEncode(const s: string): string; function TFTimeEntries.HtmlEncode(const s: string): string;
begin begin
Result := s; Result := s;
...@@ -123,6 +148,7 @@ begin ...@@ -123,6 +148,7 @@ begin
Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]); Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end; end;
function TFTimeEntries.IsoToDate(const AValue: string): TDateTime; function TFTimeEntries.IsoToDate(const AValue: string): TDateTime;
var var
yearValue: Integer; yearValue: Integer;
...@@ -217,8 +243,7 @@ var ...@@ -217,8 +243,7 @@ var
el: TJSHTMLElement; el: TJSHTMLElement;
begin begin
el := TJSHTMLElement(document.getElementById('lbl_time_total_rows')); el := TJSHTMLElement(document.getElementById('lbl_time_total_rows'));
if Assigned(el) then el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
end; end;
procedure TFTimeEntries.SetTimeEntriesLabel(const AName: string); procedure TFTimeEntries.SetTimeEntriesLabel(const AName: string);
...@@ -231,8 +256,7 @@ begin ...@@ -231,8 +256,7 @@ begin
displayName := 'User'; displayName := 'User';
el := TJSHTMLElement(document.getElementById('lbl_time_entries_title')); el := TJSHTMLElement(document.getElementById('lbl_time_entries_title'));
if Assigned(el) then el.innerText := displayName + '''s Time Entries';
el.innerText := displayName + '''s Time Entries';
end; end;
[async] procedure TFTimeEntries.LoadTimeEntries; [async] procedure TFTimeEntries.LoadTimeEntries;
...@@ -265,41 +289,29 @@ begin ...@@ -265,41 +289,29 @@ begin
end; end;
end; end;
if not Assigned(response.Result) then
Exit;
resultObj := TJSObject(response.Result); resultObj := TJSObject(response.Result);
if resultObj.hasOwnProperty('userName') then userNameValue := string(resultObj['userName']);
begin if userNameValue <> '' then
userNameValue := string(resultObj['userName']); SetTimeEntriesLabel(userNameValue)
if userNameValue <> '' then
SetTimeEntriesLabel(userNameValue);
end
else else
SetTimeEntriesLabel(FUserName); SetTimeEntriesLabel(FUserName);
rowCount := StrToIntDef(string(resultObj['count']), 0); rowCount := StrToIntDef(string(resultObj['count']), 0);
SetTotalRowsLabel(rowCount); SetTotalRowsLabel(rowCount);
if resultObj.hasOwnProperty('taskOptions') then FTaskOptions := TJSArray(resultObj['taskOptions']);
FTaskOptions := TJSArray(resultObj['taskOptions']) FCategoryOptions := TJSArray(resultObj['categoryOptions']);
else
FTaskOptions := TJSArray.new;
if resultObj.hasOwnProperty('categoryOptions') then
FCategoryOptions := TJSArray(resultObj['categoryOptions'])
else
FCategoryOptions := TJSArray.new;
itemsArray := TJSArray(resultObj['items']); itemsArray := TJSArray(resultObj['items']);
if not Assigned(itemsArray) then
itemsArray := TJSArray.new;
xdwdsTimeEntries.Close; xdwdsTimeEntries.Close;
xdwdsTimeEntries.SetJsonData(itemsArray); xdwdsTimeEntries.SetJsonData(itemsArray);
xdwdsTimeEntries.Open; xdwdsTimeEntries.Open;
FActiveRowIndex := -1;
HideRowValidationMessage;
SetTopControlsEnabled(True);
RenderTable; RenderTable;
finally finally
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
...@@ -331,6 +343,8 @@ begin ...@@ -331,6 +343,8 @@ begin
[FUserId, edtWeekOf.Text] [FUserId, edtWeekOf.Text]
)); ));
FPendingEntryId := JS.toString(response.Result);
console.log('AddTimeEntry response=' + string(TJSJSON.stringify(response.Result))); console.log('AddTimeEntry response=' + string(TJSJSON.stringify(response.Result)));
Result := True; Result := True;
except except
...@@ -368,7 +382,7 @@ var ...@@ -368,7 +382,7 @@ var
function TextInput(const FieldName, Value: string; const AIdx: Integer): string; function TextInput(const FieldName, Value: string; const AIdx: Integer): string;
begin begin
Result := Result :=
'<input class="form-control form-control-sm cell-input time-editor w-100" readonly ' + '<input class="form-control form-control-sm cell-input time-editor w-100" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' + 'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(Value) + '">'; 'value="' + HtmlEncode(Value) + '">';
end; end;
...@@ -380,7 +394,7 @@ var ...@@ -380,7 +394,7 @@ var
dateValue := Utils.NormalizeDateValue(Value); dateValue := Utils.NormalizeDateValue(Value);
Result := Result :=
'<input type="date" class="form-control form-control-sm cell-input time-editor w-100" readonly ' + '<input type="date" class="form-control form-control-sm cell-input time-editor w-100" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' + 'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(dateValue) + '">'; 'value="' + HtmlEncode(dateValue) + '">';
end; end;
...@@ -421,7 +435,7 @@ var ...@@ -421,7 +435,7 @@ var
'data-display="" ' + 'data-display="" ' +
'data-trigger-id="' + triggerId + '"></button>'; 'data-trigger-id="' + triggerId + '"></button>';
if Assigned(Items) then
for i := 0 to Items.length - 1 do for i := 0 to Items.length - 1 do
begin begin
optionObj := TJSObject(Items[i]); optionObj := TJSObject(Items[i]);
...@@ -445,15 +459,13 @@ var ...@@ -445,15 +459,13 @@ var
function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string; function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string;
begin begin
Result := Result :=
'<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100" readonly ' + '<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' + 'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'style="min-height:31px; height:31px; overflow:hidden; resize:none;">' + HtmlEncode(Value) + '</textarea>'; 'style="min-height:31px; height:31px; overflow:hidden; resize:none;">' + HtmlEncode(Value) + '</textarea>';
end; end;
begin begin
host := TJSHTMLElement(document.getElementById('time_entries_table_host')); host := TJSHTMLElement(document.getElementById('time_entries_table_host'));
if not Assigned(host) then
Exit;
html := html :=
'<div class="time-vscroll">' + '<div class="time-vscroll">' +
...@@ -486,7 +498,7 @@ begin ...@@ -486,7 +498,7 @@ begin
hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat); hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat);
html := html + html := html +
'<tr data-entry-id="' + IntToStr(xdwdsTimeEntriesentryId.AsInteger) + '" data-task-id="' + xdwdsTimeEntriestaskId.AsString + '">' + '<tr class="time-row-selectable" data-idx="' + IntToStr(rowIdx) + '" data-entry-id="' + IntToStr(xdwdsTimeEntriesentryId.AsInteger) + '" data-task-id="' + xdwdsTimeEntriestaskId.AsString + '">' +
TdNowrap(DateInput('taskDate', xdwdsTimeEntriestaskDate.AsString, rowIdx)) + TdNowrap(DateInput('taskDate', xdwdsTimeEntriestaskDate.AsString, rowIdx)) +
TdNowrap(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay')) + TdNowrap(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay')) +
TdNowrap(HoursInput('hours', hoursText, rowIdx)) + TdNowrap(HoursInput('hours', hoursText, rowIdx)) +
...@@ -506,6 +518,7 @@ begin ...@@ -506,6 +518,7 @@ begin
EnableAutoGrowTextAreas; EnableAutoGrowTextAreas;
EnableColumnResize; EnableColumnResize;
RestoreTableScroll; RestoreTableScroll;
ApplyPendingEntryFocus;
end; end;
procedure TFTimeEntries.BindTableEditors; procedure TFTimeEntries.BindTableEditors;
...@@ -518,15 +531,202 @@ begin ...@@ -518,15 +531,202 @@ begin
nodes := document.querySelectorAll('.time-editor'); nodes := document.querySelectorAll('.time-editor');
console.log('BindTableEditors: time-editor count=' + IntToStr(nodes.length)); console.log('BindTableEditors: time-editor count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput));
end;
nodes := document.querySelectorAll('.time-dd-item'); nodes := document.querySelectorAll('.time-dd-item');
console.log('BindTableEditors: time-dd-item count=' + IntToStr(nodes.length)); console.log('BindTableEditors: time-dd-item count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do for i := 0 to nodes.length - 1 do
begin begin
el := TJSHTMLElement(nodes.item(i)); el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownItemClick)); el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
end; end;
nodes := document.querySelectorAll('.time-row-selectable');
console.log('BindTableEditors: time-row-selectable count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@RowClick));
el.addEventListener('focusout', TJSEventHandler(@RowFocusOut));
end;
end;
procedure TFTimeEntries.EditorInput(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr: string;
fieldName: string;
newVal: string;
begin
if not xdwdsTimeEntries.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;
FActiveRowIndex := idx;
GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then
Exit;
newVal := string(TJSObject(el)['value']);
xdwdsTimeEntries.Edit;
if SameText(fieldName, 'taskDate') then
xdwdsTimeEntriestaskDate.AsString := newVal
else if SameText(fieldName, 'hours') then
begin
if Trim(newVal) = '' then
xdwdsTimeEntrieshours.Clear
else
xdwdsTimeEntrieshours.AsFloat := StrToFloatDef(newVal, 0);
end
else if SameText(fieldName, 'taskTime') then
xdwdsTimeEntriestaskTime.AsString := newVal
else if SameText(fieldName, 'summary') then
xdwdsTimeEntriessummary.AsString := newVal
else
xdwdsTimeEntries.FieldByName(fieldName).AsString := newVal;
xdwdsTimeEntries.Post;
el.setAttribute('data-unsaved-data', '1');
if ValidateRow(idx) then
begin
ClearRowValidation(idx);
SetTopControlsEnabled(True);
end
else
SetTopControlsEnabled(False);
end;
[async] procedure TFTimeEntries.SaveRow(AIndex: Integer);
var
saveObj: TJSObject;
response: TXDataClientResponse;
rowEl: TJSHTMLElement;
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
if not ValidateRow(AIndex) then
begin
ApplyRowValidation(AIndex);
ShowRowValidationMessage(AIndex, 'Complete required fields before leaving this row.');
Exit;
end;
saveObj := TJSObject.new;
saveObj['entryId'] := xdwdsTimeEntriesentryId.AsString;
saveObj['userId'] := FUserId;
saveObj['taskDate'] := xdwdsTimeEntriestaskDate.AsString;
saveObj['taskId'] := xdwdsTimeEntriestaskId.AsString;
saveObj['hours'] := xdwdsTimeEntrieshours.AsFloat;
saveObj['taskTime'] := xdwdsTimeEntriestaskTime.AsString;
saveObj['category'] := xdwdsTimeEntriescategory.AsString;
saveObj['summary'] := xdwdsTimeEntriessummary.AsString;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.SaveTimeEntry',
[saveObj]
));
console.log('SaveRow response=' + string(TJSJSON.stringify(response.Result)));
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
nodes := rowEl.querySelectorAll('[data-unsaved-data="1"]');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.removeAttribute('data-unsaved-data');
end;
ClearRowValidation(AIndex);
except
on E: EXDataClientRequestException do
begin
console.log('SaveRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTimeEntries.RowClick(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
idx: Integer;
begin
rowEl := TJSHTMLElement(Event.currentTarget);
idx := StrToIntDef(string(rowEl.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
FActiveRowIndex := idx;
end;
procedure TFTimeEntries.RowFocusOut(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
idx: Integer;
begin
rowEl := TJSHTMLElement(Event.currentTarget);
window.setTimeout(
procedure
begin
idx := StrToIntDef(string(rowEl.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
if (FLastMouseDownRowIndex = idx) and ((TJSDate.now - FLastMouseDownTime) < 500) then
Exit;
if Assigned(document.activeElement) and rowEl.contains(document.activeElement) then
Exit;
if not ValidateRow(idx) then
begin
FActiveRowIndex := idx;
ApplyRowValidation(idx);
ShowRowValidationMessage(idx, 'Complete required fields before leaving this row.');
SetTopControlsEnabled(False);
Exit;
end;
ClearRowValidation(idx);
SetTopControlsEnabled(True);
if Assigned(rowEl.querySelector('[data-unsaved-data="1"]')) then
SaveRow(idx);
end,
0
);
end; end;
...@@ -580,16 +780,126 @@ begin ...@@ -580,16 +780,126 @@ begin
xdwdsTimeEntries.Post; xdwdsTimeEntries.Post;
if triggerId <> '' then FActiveRowIndex := idx;
btn := TJSHTMLElement(document.getElementById(triggerId));
btn.setAttribute('data-unsaved-data', '1');
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label'));
labelEl.textContent := newDisplay;
btn.focus;
if ValidateRow(idx) then
begin begin
btn := TJSHTMLElement(document.getElementById(triggerId)); ClearRowValidation(idx);
if Assigned(btn) then SetTopControlsEnabled(True);
begin end
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label')); else
if Assigned(labelEl) then SetTopControlsEnabled(False);
labelEl.textContent := newDisplay; end;
end;
function TFTimeEntries.ValidateRow(AIndex: Integer): Boolean;
var
hasDate: Boolean;
hasTask: Boolean;
hasHours: Boolean;
hasCategory: Boolean;
hasSummary: Boolean;
begin
Result := False;
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
hasDate := Trim(xdwdsTimeEntriestaskDate.AsString) <> '';
hasTask := Trim(xdwdsTimeEntriestaskId.AsString) <> '';
if xdwdsTimeEntrieshours.IsNull then
hasHours := False
else
hasHours := xdwdsTimeEntrieshours.AsFloat > 0;
hasCategory := Trim(xdwdsTimeEntriescategory.AsString) <> '';
hasSummary := Trim(xdwdsTimeEntriessummary.AsString) <> '';
Result := hasDate and hasTask and hasHours and hasCategory and hasSummary;
end;
procedure TFTimeEntries.ApplyPendingEntryFocus;
var
rowEl: TJSHTMLElement;
idx: Integer;
firstEditor: TJSHTMLElement;
begin
if FPendingEntryId = '' then
Exit;
rowEl := TJSHTMLElement(document.querySelector('tr[data-entry-id="' + FPendingEntryId + '"]'));
if not Assigned(rowEl) then
begin
FPendingEntryId := '';
Exit;
end; end;
idx := StrToIntDef(string(rowEl.getAttribute('data-idx')), -1);
if idx < 0 then
begin
FPendingEntryId := '';
Exit;
end;
FActiveRowIndex := idx;
SetTopControlsEnabled(False);
firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="taskDate"]'));
if Assigned(firstEditor) then
firstEditor.focus;
FPendingEntryId := '';
end;
procedure TFTimeEntries.ApplyRowValidation(AIndex: Integer);
var
rowEl: TJSHTMLElement;
hoursInvalid: Boolean;
procedure SetInvalid(const AFieldName: string; AInvalid: Boolean);
var
fieldEl: TJSHTMLElement;
begin
fieldEl := TJSHTMLElement(rowEl.querySelector('[data-field="' + AFieldName + '"]'));
if AInvalid then
fieldEl.classList.add('is-invalid')
else
fieldEl.classList.remove('is-invalid');
end;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
if xdwdsTimeEntrieshours.IsNull then
hoursInvalid := True
else
hoursInvalid := xdwdsTimeEntrieshours.AsFloat <= 0;
SetInvalid('taskDate', Trim(xdwdsTimeEntriestaskDate.AsString) = '');
SetInvalid('taskId', Trim(xdwdsTimeEntriestaskId.AsString) = '');
SetInvalid('hours', hoursInvalid);
SetInvalid('category', Trim(xdwdsTimeEntriescategory.AsString) = '');
SetInvalid('summary', Trim(xdwdsTimeEntriessummary.AsString) = '');
end; end;
...@@ -605,7 +915,6 @@ begin ...@@ -605,7 +915,6 @@ begin
editor.style.height = 'auto'; editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px'; editor.style.height = editor.scrollHeight + 'px';
}; };
fit(); fit();
editor.addEventListener('input', fit); editor.addEventListener('input', fit);
}); });
...@@ -613,6 +922,79 @@ begin ...@@ -613,6 +922,79 @@ begin
end; end;
end; end;
procedure TFTimeEntries.ClearRowValidation(AIndex: Integer);
var
rowEl: TJSHTMLElement;
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
nodes := rowEl.querySelectorAll('.is-invalid');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.classList.remove('is-invalid');
end;
SetTopControlsEnabled(True);
HideRowValidationMessage;
end;
procedure TFTimeEntries.ShowRowValidationMessage(AIndex: Integer; const AMessage: string);
var
rowEl: TJSHTMLElement;
firstInvalidEl: TJSHTMLElement;
begin
lblValidationMessage.Caption := AMessage;
lblValidationMessage.ElementHandle.classList.remove('d-none');
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
firstInvalidEl := TJSHTMLElement(rowEl.querySelector('.is-invalid'));
firstInvalidEl.focus;
end;
procedure TFTimeEntries.HideRowValidationMessage;
begin
lblValidationMessage.Caption := '';
lblValidationMessage.ElementHandle.classList.add('d-none');
end;
function TFTimeEntries.GetTargetRowIndex(ATarget: TJSHTMLElement): Integer;
var
el: TJSHTMLElement;
idxText: string;
begin
Result := -1;
el := ATarget;
while Assigned(el) do
begin
idxText := string(el.getAttribute('data-idx'));
if idxText <> '' then
begin
Result := StrToIntDef(idxText, -1);
if Result >= 0 then
Exit;
end;
el := TJSHTMLElement(el.parentElement);
end;
end;
procedure TFTimeEntries.DocumentMouseDown(Event: TJSEvent);
begin
FLastMouseDownRowIndex := GetTargetRowIndex(TJSHTMLElement(Event.target));
FLastMouseDownTime := TJSDate.now;
end;
procedure TFTimeEntries.EnableColumnResize; procedure TFTimeEntries.EnableColumnResize;
begin begin
asm asm
...@@ -665,6 +1047,7 @@ begin ...@@ -665,6 +1047,7 @@ begin
end; end;
end; end;
[async] procedure TFTimeEntries.btnAddEntryClick(Sender: TObject); [async] procedure TFTimeEntries.btnAddEntryClick(Sender: TObject);
begin begin
Utils.ShowSpinner('spinner'); Utils.ShowSpinner('spinner');
...@@ -679,6 +1062,7 @@ begin ...@@ -679,6 +1062,7 @@ begin
end; end;
end; end;
procedure TFTimeEntries.CaptureTableScroll; procedure TFTimeEntries.CaptureTableScroll;
begin begin
asm asm
...@@ -690,6 +1074,7 @@ begin ...@@ -690,6 +1074,7 @@ begin
end; end;
end; end;
procedure TFTimeEntries.RestoreTableScroll; procedure TFTimeEntries.RestoreTableScroll;
begin begin
asm asm
...@@ -701,4 +1086,12 @@ begin ...@@ -701,4 +1086,12 @@ begin
end; end;
end; end;
procedure TFTimeEntries.SetTopControlsEnabled(AEnabled: Boolean);
begin
edtWeekOf.Enabled := AEnabled;
edtStartDate.Enabled := AEnabled;
edtEndDate.Enabled := AEnabled;
btnAddEntry.Enabled := AEnabled;
end;
end. end.
...@@ -44,4 +44,9 @@ ...@@ -44,4 +44,9 @@
.time-table .dropdown-menu { .time-table .dropdown-menu {
z-index: 1055; z-index: 1055;
}
.time-dd-toggle.is-invalid {
border-color: var(--bs-danger) !important;
padding-right: calc(1.5em + .75rem);
} }
\ No newline at end of file
...@@ -1142,4 +1142,68 @@ object ApiDatabase: TApiDatabase ...@@ -1142,4 +1142,68 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqSaveTimeEntry: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'UPDATE time_items'
'SET'
' TASK_DATE = :TASK_DATE,'
' TASK_ID = :TASK_ID,'
' HOURS = :HOURS,'
' TASK_TIME = :TASK_TIME,'
' CATEGORY = :CATEGORY,'
' SUMMARY = :SUMMARY,'
' MODIFY_DATE = now(),'
' MODIFIED_BY = :MODIFIED_BY'
'WHERE ENTRY_ID = :ENTRY_ID'
' AND USER_ID = :USER_ID')
Left = 544
Top = 424
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_DATE'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'HOURS'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_TIME'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY'
Value = nil
end
item
DataType = ftUnknown
Name = 'SUMMARY'
Value = nil
end
item
DataType = ftUnknown
Name = 'MODIFIED_BY'
Value = nil
end
item
DataType = ftUnknown
Name = 'ENTRY_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
end
end end
...@@ -89,6 +89,7 @@ type ...@@ -89,6 +89,7 @@ type
uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField; uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField;
uqProjectReportedUsersNAME: TStringField; uqProjectReportedUsersNAME: TStringField;
uqAddTimeEntry: TUniQuery; uqAddTimeEntry: TUniQuery;
uqSaveTimeEntry: TUniQuery;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -48,11 +48,24 @@ type ...@@ -48,11 +48,24 @@ type
destructor Destroy; override; destructor Destroy; override;
end; end;
TTimeEntrySave = class
public
entryId: string;
userId: string;
taskDate: string;
taskId: string;
hours: Double;
taskTime: string;
category: string;
summary: string;
end;
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
ITimeEntryService = interface(IInvokable) ITimeEntryService = interface(IInvokable)
['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'] ['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}']
[HttpGet] function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse; [HttpGet] function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
[HttpPost] function AddTimeEntry(userId, taskDate: string): string; [HttpPost] function AddTimeEntry(userId, taskDate: string): string;
[HttpPost] function SaveTimeEntry(Item: TTimeEntrySave): Boolean;
end; end;
implementation implementation
......
...@@ -31,6 +31,7 @@ type ...@@ -31,6 +31,7 @@ type
public public
function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse; function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
function AddTimeEntry(userId, taskDate: string): string; function AddTimeEntry(userId, taskDate: string): string;
function SaveTimeEntry(Item: TTimeEntrySave): Boolean;
end; end;
implementation implementation
...@@ -429,6 +430,38 @@ begin ...@@ -429,6 +430,38 @@ begin
end; end;
function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): Boolean;
var
taskDateValue: TDateTime;
begin
Logger.Log(4, Format('TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"', [Item.entryId, Item.userId]));
taskDateValue := ParseIsoDate(Item.taskDate);
apiDB.uqSaveTimeEntry.Close;
apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := Item.entryId;
apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId;
apiDB.uqSaveTimeEntry.ParamByName('HOURS').AsFloat := Item.hours;
if Trim(Item.taskTime) = '' then
apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').Clear
else
apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').AsString := Item.taskTime;
apiDB.uqSaveTimeEntry.ParamByName('CATEGORY').AsString := Item.category;
apiDB.uqSaveTimeEntry.ParamByName('SUMMARY').AsString := Item.summary;
apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ExecSQL;
Result := True;
Logger.Log(4, 'TimeEntryService.SaveTimeEntry - saved ENTRY_ID=' + Item.entryId);
end;
initialization initialization
RegisterServiceType(TTimeEntryService); RegisterServiceType(TTimeEntryService);
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.8.9 webClientVersion=0.8.9
LogFileNum=219 LogFileNum=222
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.131
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment