Commit 892b81cb by Mac Stephens

update add row to save row with save and cancel buttons change check logic for…

update add row to save row with save and cancel buttons change check logic for validation, ver0.9.0.0 created
parent 7a8e3164
...@@ -34,9 +34,20 @@ ...@@ -34,9 +34,20 @@
</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="lbl_validation_message" class="alert alert-danger py-1 px-2 mb-2 invisible small" style="min-height: 31px;"></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 id="time_new_entry_actions" class="position-fixed d-none" style="z-index: 1030;">
<div class="bg-body border rounded shadow-sm p-2 d-flex gap-2">
<button type="button" id="btn_time_new_save" class="btn btn-sm btn-success">
Save
</button>
<button type="button" id="btn_time_new_cancel" class="btn btn-sm btn-outline-secondary">
Cancel
</button>
</div>
</div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="task_picker_offcanvas" aria-labelledby="task_picker_title"> <div class="offcanvas offcanvas-end" tabindex="-1" id="task_picker_offcanvas" aria-labelledby="task_picker_title">
<div class="offcanvas-header"> <div class="offcanvas-header">
<h5 class="offcanvas-title" id="task_picker_title">Find Task</h5> <h5 class="offcanvas-title" id="task_picker_title">Find Task</h5>
......
...@@ -30,7 +30,7 @@ type ...@@ -30,7 +30,7 @@ type
btnDeleteEntry: TWebButton; btnDeleteEntry: TWebButton;
btnSearchRange: TWebButton; btnSearchRange: TWebButton;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
[async] procedure btnAddEntryClick(Sender: TObject); procedure btnAddEntryClick(Sender: TObject);
procedure btnDeleteEntryClick(Sender: TObject); procedure btnDeleteEntryClick(Sender: TObject);
procedure edtWeekOfExit(Sender: TObject); procedure edtWeekOfExit(Sender: TObject);
[async] procedure btnSearchRangeClick(Sender: TObject); [async] procedure btnSearchRangeClick(Sender: TObject);
...@@ -46,12 +46,23 @@ type ...@@ -46,12 +46,23 @@ type
FPendingScrollTop: Integer; FPendingScrollTop: Integer;
FPendingScrollLeft: Integer; FPendingScrollLeft: Integer;
FActiveRowIndex: Integer; FActiveRowIndex: Integer;
FBlockedRowIndex: Integer;
FPendingEntryId: string; FPendingEntryId: string;
FLastMouseDownRowIndex: Integer; FLastMouseDownRowIndex: Integer;
FLastMouseDownTime: Double; FLastMouseDownTime: Double;
FTaskPickerOffCanvas: TTaskPickerOffCanvas; FTaskPickerOffCanvas: TTaskPickerOffCanvas;
FAddingNewEntry: Boolean;
FNewEntryRowIndex: Integer;
[async] procedure LoadTimeEntries; [async] procedure LoadTimeEntries;
[async] function AddTimeEntry: Boolean; function BeginAddEntry: Boolean;
function IsNewEntryRow(AIndex: Integer): Boolean;
procedure ResetNewEntryState;
procedure BindNewEntryButtons;
procedure NewEntrySaveClick(Event: TJSEvent);
procedure NewEntryCancelClick(Event: TJSEvent);
[async] procedure SaveNewEntry;
procedure CancelNewEntry;
procedure RenderTable; procedure RenderTable;
procedure BindTableEditors; procedure BindTableEditors;
procedure DropdownItemClick(Event: TJSEvent); procedure DropdownItemClick(Event: TJSEvent);
...@@ -112,14 +123,19 @@ begin ...@@ -112,14 +123,19 @@ begin
FPendingScrollTop := 0; FPendingScrollTop := 0;
FPendingScrollLeft := 0; FPendingScrollLeft := 0;
FActiveRowIndex := -1; FActiveRowIndex := -1;
FBlockedRowIndex := -1;
FPendingEntryId := ''; FPendingEntryId := '';
FLastMouseDownRowIndex := -1; FLastMouseDownRowIndex := -1;
FLastMouseDownTime := 0; FLastMouseDownTime := 0;
FAddingNewEntry := False;
FNewEntryRowIndex := -1;
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected); FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown)); document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
BindNewEntryButtons;
payload := AuthService.TokenPayload; payload := AuthService.TokenPayload;
if Assigned(payload) then if Assigned(payload) then
begin begin
...@@ -323,53 +339,6 @@ begin ...@@ -323,53 +339,6 @@ begin
end; end;
[async] function TFTimeEntries.AddTimeEntry: Boolean;
var
response: TXDataClientResponse;
resultObj: TJSObject;
begin
Result := False;
if FUserId = '' then
begin
Utils.ShowErrorModal('Unable to determine logged-in user.');
Exit;
end;
if edtWeekOf.Text = '' then
begin
Utils.ShowErrorModal('Select a week/date before adding an entry.');
Exit;
end;
if (FActiveRowIndex >= 0) and (not ValidateRow(FActiveRowIndex)) then
begin
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before adding another entry.');
SetTopControlsEnabled(False);
Exit;
end;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.AddTimeEntry',
[FUserId, edtWeekOf.Text]
));
resultObj := TJSObject(response.Result);
FPendingEntryId := string(resultObj['value']);
console.log('AddTimeEntry server entry id=' + FPendingEntryId);
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('AddTimeEntry ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTimeEntries.RenderTable; procedure TFTimeEntries.RenderTable;
...@@ -378,6 +347,10 @@ var ...@@ -378,6 +347,10 @@ var
html: string; html: string;
rowIdx: Integer; rowIdx: Integer;
hoursText: string; hoursText: string;
actionsEl: TJSHTMLElement;
rowEl: TJSHTMLElement;
rowRect: TJSObject;
topPos: Integer;
function Th(const s: string): string; function Th(const s: string): string;
begin begin
...@@ -531,16 +504,16 @@ begin ...@@ -531,16 +504,16 @@ begin
else else
hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat); hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat);
html := html + html := html +
'<tr class="time-row-selectable" data-idx="' + IntToStr(rowIdx) + '" 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', False, True)) + TdNowrap(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay', False, True)) +
TdNowrap(HoursInput('hours', hoursText, rowIdx)) + TdNowrap(HoursInput('hours', hoursText, rowIdx)) +
TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) + TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) +
TdNowrap(SelectList('place', xdwdsTimeEntriesplace.AsString, xdwdsTimeEntriesplaceDesc.AsString, rowIdx, FPlaceOptions, 'code', 'codeDesc', True, False)) + TdNowrap(SelectList('place', xdwdsTimeEntriesplace.AsString, xdwdsTimeEntriesplaceDesc.AsString, rowIdx, FPlaceOptions, 'code', 'codeDesc', True, False)) +
TdNowrap(SelectList('category', xdwdsTimeEntriescategory.AsString, xdwdsTimeEntriescategoryDesc.AsString, rowIdx, FCategoryOptions, 'code', 'codeDesc', False, False)) + TdNowrap(SelectList('category', xdwdsTimeEntriescategory.AsString, xdwdsTimeEntriescategoryDesc.AsString, rowIdx, FCategoryOptions, 'code', 'codeDesc', False, False)) +
TdWrap(SummaryTextArea('summary', xdwdsTimeEntriessummary.AsString, rowIdx)) + TdWrap(SummaryTextArea('summary', xdwdsTimeEntriessummary.AsString, rowIdx)) +
'</tr>'; '</tr>';
xdwdsTimeEntries.Next; xdwdsTimeEntries.Next;
Inc(rowIdx); Inc(rowIdx);
...@@ -550,6 +523,36 @@ begin ...@@ -550,6 +523,36 @@ begin
SetTotalRowsLabel(rowIdx); SetTotalRowsLabel(rowIdx);
host.innerHTML := html; host.innerHTML := html;
actionsEl := TJSHTMLElement(document.getElementById('time_new_entry_actions'));
if Assigned(actionsEl) then
begin
if FAddingNewEntry then
begin
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(FNewEntryRowIndex) + '"]'));
if Assigned(rowEl) then
begin
actionsEl.classList.remove('d-none');
rowRect := TJSObject(rowEl.getBoundingClientRect);
topPos := Round(JS.toNumber(rowRect['bottom']) - 2);
actionsEl.setAttribute(
'style',
'z-index: 1030; ' +
'right: 12px; ' +
'top: ' + IntToStr(topPos) + 'px; ' +
'left: auto; ' +
'bottom: auto;'
);
end
else
actionsEl.classList.add('d-none');
end
else
actionsEl.classList.add('d-none');
end;
BindTableEditors; BindTableEditors;
EnableAutoGrowTextAreas; EnableAutoGrowTextAreas;
EnableColumnResize; EnableColumnResize;
...@@ -558,6 +561,135 @@ begin ...@@ -558,6 +561,135 @@ begin
ApplyActiveRowState; ApplyActiveRowState;
end; end;
procedure TFTimeEntries.BindNewEntryButtons;
var
btnSave: TJSHTMLElement;
btnCancel: TJSHTMLElement;
begin
btnSave := TJSHTMLElement(document.getElementById('btn_time_new_save'));
if Assigned(btnSave) then
btnSave.addEventListener('click', TJSEventHandler(@NewEntrySaveClick));
btnCancel := TJSHTMLElement(document.getElementById('btn_time_new_cancel'));
if Assigned(btnCancel) then
btnCancel.addEventListener('click', TJSEventHandler(@NewEntryCancelClick));
end;
procedure TFTimeEntries.NewEntrySaveClick(Event: TJSEvent);
begin
Event.preventDefault;
Event.stopPropagation;
SaveNewEntry;
end;
procedure TFTimeEntries.NewEntryCancelClick(Event: TJSEvent);
begin
Event.preventDefault;
Event.stopPropagation;
Utils.ShowConfirmationModal(
'Discard the new unsaved time entry?',
'Discard',
'Cancel',
procedure(AConfirmed: Boolean)
begin
if AConfirmed then
CancelNewEntry;
end
);
end;
[async] procedure TFTimeEntries.SaveNewEntry;
var
response: TXDataClientResponse;
resultObj: TJSObject;
payload: TJSObject;
begin
if not FAddingNewEntry then
Exit;
GotoRowIndex(FNewEntryRowIndex);
if xdwdsTimeEntries.Eof then
Exit;
if not ValidateRow(FNewEntryRowIndex) then
begin
ApplyRowValidation(FNewEntryRowIndex);
ShowRowValidationMessage(FNewEntryRowIndex, 'Complete required fields before saving this entry.');
SetTopControlsEnabled(False);
Exit;
end;
payload := TJSObject.new;
payload['entryId'] := '0';
payload['userId'] := FUserId;
payload['taskDate'] := xdwdsTimeEntriestaskDate.AsString;
payload['taskId'] := xdwdsTimeEntriestaskId.AsString;
payload['hours'] := xdwdsTimeEntrieshours.AsFloat;
payload['taskTime'] := xdwdsTimeEntriestaskTime.AsString;
payload['place'] := xdwdsTimeEntriesplace.AsString;
payload['category'] := xdwdsTimeEntriescategory.AsString;
payload['summary'] := xdwdsTimeEntriessummary.AsString;
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.SaveTimeEntry',
[payload]
));
resultObj := TJSObject(response.Result);
FPendingEntryId := string(resultObj['value']);
except
on E: EXDataClientRequestException do
begin
console.log('SaveNewEntry ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
Exit;
end;
end;
ResetNewEntryState;
FActiveRowIndex := -1;
HideRowValidationMessage;
SetTopControlsEnabled(True);
btnDeleteEntry.Enabled := False;
CaptureTableScroll;
await(LoadTimeEntries);
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTimeEntries.CancelNewEntry;
begin
if not FAddingNewEntry then
Exit;
if xdwdsTimeEntries.Active then
begin
GotoRowIndex(FNewEntryRowIndex);
if not xdwdsTimeEntries.Eof then
xdwdsTimeEntries.Delete;
end;
ResetNewEntryState;
FActiveRowIndex := -1;
HideRowValidationMessage;
SetTopControlsEnabled(True);
btnDeleteEntry.Enabled := False;
RenderTable;
end;
procedure TFTimeEntries.BindTableEditors; procedure TFTimeEntries.BindTableEditors;
var var
nodes: TJSNodeList; nodes: TJSNodeList;
...@@ -623,7 +755,7 @@ begin ...@@ -623,7 +755,7 @@ begin
Exit; Exit;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := not IsNewEntryRow(idx);
GotoRowIndex(idx); GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then if xdwdsTimeEntries.Eof then
...@@ -653,6 +785,15 @@ begin ...@@ -653,6 +785,15 @@ begin
el.setAttribute('data-unsaved-data', '1'); el.setAttribute('data-unsaved-data', '1');
if IsNewEntryRow(idx) then
begin
if ValidateRow(idx) then
ClearRowValidation(idx);
SetTopControlsEnabled(False);
Exit;
end;
if ValidateRow(idx) then if ValidateRow(idx) then
begin begin
ClearRowValidation(idx); ClearRowValidation(idx);
...@@ -674,6 +815,25 @@ begin ...@@ -674,6 +815,25 @@ begin
if idx < 0 then if idx < 0 then
Exit; Exit;
if FAddingNewEntry and (idx <> FNewEntryRowIndex) then
begin
Event.preventDefault;
Event.stopPropagation;
FActiveRowIndex := FNewEntryRowIndex;
btnDeleteEntry.Enabled := False;
SetTopControlsEnabled(False);
FBlockedRowIndex := FNewEntryRowIndex;
lblValidationMessage.Caption := 'Save or cancel the new entry before selecting another row.';
lblValidationMessage.ElementHandle.classList.remove('d-none');
lblValidationMessage.ElementHandle.classList.remove('invisible');
ApplyActiveRowState;
Exit;
end;
if (FActiveRowIndex >= 0) and if (FActiveRowIndex >= 0) and
(idx <> FActiveRowIndex) and (idx <> FActiveRowIndex) and
(not ValidateRow(FActiveRowIndex)) then (not ValidateRow(FActiveRowIndex)) then
...@@ -688,7 +848,7 @@ begin ...@@ -688,7 +848,7 @@ begin
end; end;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := not IsNewEntryRow(idx);
ApplyActiveRowState; ApplyActiveRowState;
end; end;
...@@ -707,6 +867,9 @@ begin ...@@ -707,6 +867,9 @@ begin
if idx < 0 then if idx < 0 then
Exit; Exit;
if IsNewEntryRow(idx) then
Exit;
if (FLastMouseDownRowIndex = idx) and ((TJSDate.now - FLastMouseDownTime) < 500) then if (FLastMouseDownRowIndex = idx) and ((TJSDate.now - FLastMouseDownTime) < 500) then
Exit; Exit;
...@@ -760,6 +923,39 @@ begin ...@@ -760,6 +923,39 @@ begin
if (idx < 0) or (fieldName = '') then if (idx < 0) or (fieldName = '') then
Exit; Exit;
if FAddingNewEntry and (idx <> FNewEntryRowIndex) then
begin
Event.preventDefault;
Event.stopPropagation;
FActiveRowIndex := FNewEntryRowIndex;
btnDeleteEntry.Enabled := False;
SetTopControlsEnabled(False);
FBlockedRowIndex := FNewEntryRowIndex;
lblValidationMessage.Caption := 'Save or cancel the new entry before changing another row.';
lblValidationMessage.ElementHandle.classList.remove('d-none');
lblValidationMessage.ElementHandle.classList.remove('invisible');
ApplyActiveRowState;
Exit;
end;
if (FActiveRowIndex >= 0) and
(idx <> FActiveRowIndex) and
(not ValidateRow(FActiveRowIndex)) then
begin
Event.preventDefault;
Event.stopPropagation;
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before changing another row.');
SetTopControlsEnabled(False);
ApplyActiveRowState;
Exit;
end;
GotoRowIndex(idx); GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then if xdwdsTimeEntries.Eof then
Exit; Exit;
...@@ -787,7 +983,7 @@ begin ...@@ -787,7 +983,7 @@ begin
xdwdsTimeEntries.Post; xdwdsTimeEntries.Post;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := not IsNewEntryRow(idx);
btn := TJSHTMLElement(document.getElementById(triggerId)); btn := TJSHTMLElement(document.getElementById(triggerId));
if Assigned(btn) then if Assigned(btn) then
...@@ -799,7 +995,17 @@ begin ...@@ -799,7 +995,17 @@ begin
btn.focus; btn.focus;
end; end;
SaveField(idx, fieldName); if not IsNewEntryRow(idx) then
SaveField(idx, fieldName);
if IsNewEntryRow(idx) then
begin
if ValidateRow(idx) then
ClearRowValidation(idx);
SetTopControlsEnabled(False);
Exit;
end;
if ValidateRow(idx) then if ValidateRow(idx) then
begin begin
...@@ -866,7 +1072,7 @@ begin ...@@ -866,7 +1072,7 @@ begin
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := True;
SetTopControlsEnabled(False); SetTopControlsEnabled(True);
ApplyActiveRowState; ApplyActiveRowState;
firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="taskId"]')); firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="taskId"]'));
...@@ -971,19 +1177,28 @@ var ...@@ -971,19 +1177,28 @@ var
rowEl: TJSHTMLElement; rowEl: TJSHTMLElement;
firstInvalidEl: TJSHTMLElement; firstInvalidEl: TJSHTMLElement;
begin begin
FBlockedRowIndex := AIndex;
lblValidationMessage.Caption := AMessage; lblValidationMessage.Caption := AMessage;
lblValidationMessage.ElementHandle.classList.remove('d-none'); lblValidationMessage.ElementHandle.classList.remove('d-none');
lblValidationMessage.ElementHandle.classList.remove('invisible');
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]')); rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
firstInvalidEl := TJSHTMLElement(rowEl.querySelector('.is-invalid')); firstInvalidEl := TJSHTMLElement(rowEl.querySelector('.is-invalid'));
firstInvalidEl.focus; firstInvalidEl.focus;
ApplyActiveRowState;
end; end;
procedure TFTimeEntries.HideRowValidationMessage; procedure TFTimeEntries.HideRowValidationMessage;
begin begin
FBlockedRowIndex := -1;
lblValidationMessage.Caption := ''; lblValidationMessage.Caption := '';
lblValidationMessage.ElementHandle.classList.add('d-none'); lblValidationMessage.ElementHandle.classList.add('invisible');
ApplyActiveRowState;
end; end;
...@@ -1019,6 +1234,29 @@ begin ...@@ -1019,6 +1234,29 @@ begin
targetEl := TJSHTMLElement(Event.target); targetEl := TJSHTMLElement(Event.target);
targetRowIndex := GetTargetRowIndex(targetEl); targetRowIndex := GetTargetRowIndex(targetEl);
if FAddingNewEntry and
(targetRowIndex >= 0) and
(targetRowIndex <> FNewEntryRowIndex) then
begin
Event.preventDefault;
Event.stopPropagation;
FActiveRowIndex := FNewEntryRowIndex;
btnDeleteEntry.Enabled := False;
SetTopControlsEnabled(False);
FBlockedRowIndex := FNewEntryRowIndex;
lblValidationMessage.Caption := 'Save or cancel the new entry before selecting another row.';
lblValidationMessage.ElementHandle.classList.remove('d-none');
lblValidationMessage.ElementHandle.classList.remove('invisible');
FLastMouseDownRowIndex := FNewEntryRowIndex;
FLastMouseDownTime := TJSDate.now;
ApplyActiveRowState;
Exit;
end;
if (FActiveRowIndex >= 0) and if (FActiveRowIndex >= 0) and
(targetRowIndex >= 0) and (targetRowIndex >= 0) and
(targetRowIndex <> FActiveRowIndex) and (targetRowIndex <> FActiveRowIndex) and
...@@ -1094,19 +1332,81 @@ begin ...@@ -1094,19 +1332,81 @@ begin
end; end;
end; end;
function TFTimeEntries.IsNewEntryRow(AIndex: Integer): Boolean;
begin
Result := FAddingNewEntry and (AIndex = FNewEntryRowIndex);
end;
[async] procedure TFTimeEntries.btnAddEntryClick(Sender: TObject); procedure TFTimeEntries.ResetNewEntryState;
begin begin
Utils.ShowSpinner('spinner'); FAddingNewEntry := False;
try FNewEntryRowIndex := -1;
if await(AddTimeEntry) then end;
begin
CaptureTableScroll;
await(LoadTimeEntries); function TFTimeEntries.BeginAddEntry: Boolean;
end; begin
finally Result := False;
Utils.HideSpinner('spinner');
if FUserId = '' then
begin
Utils.ShowErrorModal('Unable to determine logged-in user.');
Exit;
end;
if edtWeekOf.Text = '' then
begin
Utils.ShowErrorModal('Select a week/date before adding an entry.');
Exit;
end;
if FAddingNewEntry then
begin
Utils.ShowErrorModal('Save or cancel the new entry before adding another entry.');
Exit;
end; end;
if (FActiveRowIndex >= 0) and (not ValidateRow(FActiveRowIndex)) then
begin
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before adding another entry.');
SetTopControlsEnabled(False);
Exit;
end;
xdwdsTimeEntries.Append;
xdwdsTimeEntriesentryId.AsInteger := 0;
xdwdsTimeEntriestaskDate.AsString := edtWeekOf.Text;
xdwdsTimeEntriestaskId.AsString := '';
xdwdsTimeEntriestaskDisplay.AsString := '';
xdwdsTimeEntrieshours.Clear;
xdwdsTimeEntriestaskTime.AsString := '';
xdwdsTimeEntriesplace.AsString := '';
xdwdsTimeEntriesplaceDesc.AsString := '';
xdwdsTimeEntriescategory.AsString := '';
xdwdsTimeEntriescategoryDesc.AsString := '';
xdwdsTimeEntriessummary.AsString := '';
xdwdsTimeEntries.Post;
FAddingNewEntry := True;
FNewEntryRowIndex := xdwdsTimeEntries.RecordCount - 1;
FActiveRowIndex := FNewEntryRowIndex;
SetTopControlsEnabled(False);
btnDeleteEntry.Enabled := False;
HideRowValidationMessage;
CaptureTableScroll;
RenderTable;
Result := True;
end;
procedure TFTimeEntries.btnAddEntryClick(Sender: TObject);
begin
BeginAddEntry;
end; end;
...@@ -1203,8 +1503,41 @@ begin ...@@ -1203,8 +1503,41 @@ begin
if idx < 0 then if idx < 0 then
Exit; Exit;
if FAddingNewEntry and (idx <> FNewEntryRowIndex) then
begin
Event.preventDefault;
Event.stopPropagation;
FActiveRowIndex := FNewEntryRowIndex;
btnDeleteEntry.Enabled := False;
SetTopControlsEnabled(False);
FBlockedRowIndex := FNewEntryRowIndex;
lblValidationMessage.Caption := 'Save or cancel the new entry before changing another row.';
lblValidationMessage.ElementHandle.classList.remove('d-none');
lblValidationMessage.ElementHandle.classList.remove('invisible');
ApplyActiveRowState;
Exit;
end;
if (FActiveRowIndex >= 0) and
(idx <> FActiveRowIndex) and
(not ValidateRow(FActiveRowIndex)) then
begin
Event.preventDefault;
Event.stopPropagation;
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before changing another row.');
SetTopControlsEnabled(False);
ApplyActiveRowState;
Exit;
end;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := not IsNewEntryRow(idx);
if not Assigned(FTaskPickerOffCanvas) then if not Assigned(FTaskPickerOffCanvas) then
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected); FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
...@@ -1231,7 +1564,7 @@ begin ...@@ -1231,7 +1564,7 @@ begin
xdwdsTimeEntries.Post; xdwdsTimeEntries.Post;
FActiveRowIndex := ARowIndex; FActiveRowIndex := ARowIndex;
btnDeleteEntry.Enabled := True; btnDeleteEntry.Enabled := not IsNewEntryRow(ARowIndex);
CaptureTableScroll; CaptureTableScroll;
RenderTable; RenderTable;
...@@ -1242,7 +1575,17 @@ begin ...@@ -1242,7 +1575,17 @@ begin
if Assigned(btn) then if Assigned(btn) then
btn.focus; btn.focus;
SaveField(ARowIndex, 'taskId'); if not IsNewEntryRow(ARowIndex) then
SaveField(ARowIndex, 'taskId');
if IsNewEntryRow(ARowIndex) then
begin
if ValidateRow(ARowIndex) then
ClearRowValidation(ARowIndex);
SetTopControlsEnabled(False);
Exit;
end;
if ValidateRow(ARowIndex) then if ValidateRow(ARowIndex) then
begin begin
...@@ -1257,14 +1600,18 @@ procedure TFTimeEntries.ApplyActiveRowState; ...@@ -1257,14 +1600,18 @@ procedure TFTimeEntries.ApplyActiveRowState;
begin begin
asm asm
const activeRowIndex = this.FActiveRowIndex; const activeRowIndex = this.FActiveRowIndex;
const blockedRowIndex = this.FBlockedRowIndex;
document.querySelectorAll('.time-row-selectable').forEach(function(row) { document.querySelectorAll('.time-row-selectable').forEach(function(row) {
const rowIndex = parseInt(row.getAttribute('data-idx') || '-1', 10); const rowIndex = parseInt(row.getAttribute('data-idx') || '-1', 10);
if ((activeRowIndex >= 0) && (rowIndex === activeRowIndex)) row.classList.remove('table-active');
row.classList.remove('table-danger');
if ((blockedRowIndex >= 0) && (rowIndex === blockedRowIndex))
row.classList.add('table-danger');
else if ((activeRowIndex >= 0) && (rowIndex === activeRowIndex))
row.classList.add('table-active'); row.classList.add('table-active');
else
row.classList.remove('table-active');
}); });
end; end;
end; end;
...@@ -1294,6 +1641,12 @@ begin ...@@ -1294,6 +1641,12 @@ begin
if idx < 0 then if idx < 0 then
Exit; Exit;
if IsNewEntryRow(idx) then
begin
console.log('EditorBlur: skip local new row idx=' + IntToStr(idx) + ' field=' + fieldName);
Exit;
end;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName); console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
SaveField(idx, fieldName); SaveField(idx, fieldName);
end; end;
......
...@@ -94,16 +94,15 @@ ...@@ -94,16 +94,15 @@
<DCC_RemoteDebug>false</DCC_RemoteDebug> <DCC_RemoteDebug>false</DCC_RemoteDebug>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo> <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.8.9.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.8.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.8.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>9</VerInfo_Release> <TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
<TMSURLParams>?time_entries=true&amp;date=2026-05-01</TMSURLParams> <TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSURLParams>?time_entries=true&amp;date=2026-05-01</TMSURLParams>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols> <DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
......
...@@ -7,8 +7,7 @@ object ApiDatabase: TApiDatabase ...@@ -7,8 +7,7 @@ object ApiDatabase: TApiDatabase
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.102.131' Server = '192.168.102.133'
Connected = True
LoginPrompt = False LoginPrompt = False
Left = 435 Left = 435
Top = 359 Top = 359
......
...@@ -103,7 +103,7 @@ object AuthDatabase: TAuthDatabase ...@@ -103,7 +103,7 @@ object AuthDatabase: TAuthDatabase
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.102.131' Server = '192.168.102.133'
LoginPrompt = False LoginPrompt = False
Left = 71 Left = 71
Top = 133 Top = 133
......
...@@ -92,7 +92,7 @@ type ...@@ -92,7 +92,7 @@ type
['{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; [HttpPost] function SaveTimeEntry(Item: TTimeEntrySave): string;
[HttpPost] function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean; [HttpPost] function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
[HttpPost] function DeleteTimeEntry(userId: string; entryId: Integer): Boolean; [HttpPost] function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse; function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse;
......
...@@ -32,7 +32,7 @@ type ...@@ -32,7 +32,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; function SaveTimeEntry(Item: TTimeEntrySave): string;
function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean; function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse; function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse;
function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse; function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse;
...@@ -476,13 +476,38 @@ begin ...@@ -476,13 +476,38 @@ begin
end; end;
function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): Boolean; function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): string;
var var
taskDateValue: TDateTime; taskDateValue: TDateTime;
entryId: string; entryId: string;
isNewEntry: Boolean; isNewEntry: Boolean;
q: TUniQuery;
begin begin
Logger.Log(4, Format('TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"', [Item.entryId, Item.userId])); if not Assigned(Item) then
raise Exception.Create('SaveTimeEntry: Item is nil.');
if Trim(Item.userId) = '' then
raise Exception.Create('SaveTimeEntry: Invalid userId.');
if Trim(Item.taskDate) = '' then
raise Exception.Create('SaveTimeEntry: Task date is required.');
if Trim(Item.taskId) = '' then
raise Exception.Create('SaveTimeEntry: Task is required.');
if Item.hours <= 0 then
raise Exception.Create('SaveTimeEntry: Hours must be greater than zero.');
if Trim(Item.category) = '' then
raise Exception.Create('SaveTimeEntry: Category is required.');
if Trim(Item.summary) = '' then
raise Exception.Create('SaveTimeEntry: Summary is required.');
Logger.Log(4, Format(
'TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"',
[Item.entryId, Item.userId]
));
taskDateValue := ParseIsoDate(Item.taskDate); taskDateValue := ParseIsoDate(Item.taskDate);
isNewEntry := StrToIntDef(Item.entryId, 0) <= 0; isNewEntry := StrToIntDef(Item.entryId, 0) <= 0;
...@@ -491,43 +516,99 @@ begin ...@@ -491,43 +516,99 @@ begin
begin begin
entryId := GetNextIdValue('TimeEntryId'); entryId := GetNextIdValue('TimeEntryId');
apiDB.uqAddTimeEntry.Close; q := TUniQuery.Create(nil);
apiDB.uqAddTimeEntry.ParamByName('ENTRY_ID').AsString := entryId; try
apiDB.uqAddTimeEntry.ParamByName('USER_ID').AsString := Item.userId; q.Connection := apiDB.ucETaskApi;
apiDB.uqAddTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue; q.SQL.Text :=
apiDB.uqAddTimeEntry.ParamByName('CREATED_BY').AsString := Item.userId; 'insert into time_items ( ' +
apiDB.uqAddTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId; ' ENTRY_ID, ' +
apiDB.uqAddTimeEntry.ExecSQL; ' USER_ID, ' +
' TASK_DATE, ' +
' TASK_ID, ' +
' PROJECT_ID, ' +
' HOURS, ' +
' TASK_TIME, ' +
' PLACE, ' +
' CATEGORY, ' +
' SUMMARY, ' +
' CREATE_DATE, ' +
' CREATED_BY, ' +
' MODIFY_DATE, ' +
' MODIFIED_BY ' +
') values ( ' +
' :ENTRY_ID, ' +
' :USER_ID, ' +
' :TASK_DATE, ' +
' :TASK_ID, ' +
' (select t.PROJECT_ID from tasks t where t.TASK_ID = :TASK_ID), ' +
' :HOURS, ' +
' :TASK_TIME, ' +
' :PLACE, ' +
' :CATEGORY, ' +
' :SUMMARY, ' +
' now(), ' +
' :CREATED_BY, ' +
' now(), ' +
' :MODIFIED_BY ' +
')';
q.ParamByName('ENTRY_ID').AsString := entryId;
q.ParamByName('USER_ID').AsString := Item.userId;
q.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
q.ParamByName('TASK_ID').AsString := Item.taskId;
q.ParamByName('HOURS').AsFloat := Item.hours;
if Trim(Item.taskTime) = '' then
q.ParamByName('TASK_TIME').Clear
else
q.ParamByName('TASK_TIME').AsString := Item.taskTime;
if Trim(Item.place) = '' then
q.ParamByName('PLACE').Clear
else
q.ParamByName('PLACE').AsString := Item.place;
q.ParamByName('CATEGORY').AsString := Item.category;
q.ParamByName('SUMMARY').AsString := Item.summary;
q.ParamByName('CREATED_BY').AsString := Item.userId;
q.ParamByName('MODIFIED_BY').AsString := Item.userId;
q.ExecSQL;
finally
q.Free;
end;
end end
else else
begin
entryId := Item.entryId; entryId := Item.entryId;
apiDB.uqSaveTimeEntry.Close; apiDB.uqSaveTimeEntry.Close;
apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := entryId; apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := entryId;
apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId; apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue; apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId; apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId;
apiDB.uqSaveTimeEntry.ParamByName('HOURS').AsFloat := Item.hours; apiDB.uqSaveTimeEntry.ParamByName('HOURS').AsFloat := Item.hours;
if Trim(Item.taskTime) = '' then if Trim(Item.taskTime) = '' then
apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').Clear apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').Clear
else else
apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').AsString := Item.taskTime; apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').AsString := Item.taskTime;
if Trim(Item.place) = '' then if Trim(Item.place) = '' then
apiDB.uqSaveTimeEntry.ParamByName('PLACE').Clear apiDB.uqSaveTimeEntry.ParamByName('PLACE').Clear
else else
apiDB.uqSaveTimeEntry.ParamByName('PLACE').AsString := Item.place; apiDB.uqSaveTimeEntry.ParamByName('PLACE').AsString := Item.place;
apiDB.uqSaveTimeEntry.ParamByName('CATEGORY').AsString := Item.category; apiDB.uqSaveTimeEntry.ParamByName('CATEGORY').AsString := Item.category;
apiDB.uqSaveTimeEntry.ParamByName('SUMMARY').AsString := Item.summary; apiDB.uqSaveTimeEntry.ParamByName('SUMMARY').AsString := Item.summary;
apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId; apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ExecSQL; apiDB.uqSaveTimeEntry.ExecSQL;
end;
Result := True; Result := entryId;
Logger.Log(4, 'TimeEntryService.SaveTimeEntry - saved ENTRY_ID=' + entryId); Logger.Log(4, 'TimeEntryService.SaveTimeEntry - saved ENTRY_ID=' + Result);
end; end;
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.8.9 webClientVersion=0.8.9
LogFileNum=236 LogFileNum=242
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.133
--Server=192.168.116.131 --Server=192.168.116.131
--Server=192.168.159.10 --Server=192.168.159.10
Database=eTask Database=eTask
......
...@@ -114,10 +114,10 @@ ...@@ -114,10 +114,10 @@
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<DCC_ExeOutput>.\bin</DCC_ExeOutput> <DCC_ExeOutput>.\bin</DCC_ExeOutput>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> <DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.8.9.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys> <VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.9.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>9</VerInfo_Release> <VerInfo_AutoIncVersion>true</VerInfo_AutoIncVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''"> <PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
......
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