Commit e56dc151 by Mac Stephens

Merge branch 'dev'

parents 21727feb 236fcdca
...@@ -13,6 +13,7 @@ emT3XDataServer/bin/static/ ...@@ -13,6 +13,7 @@ emT3XDataServer/bin/static/
emT3XDataServer/Source/__history/ emT3XDataServer/Source/__history/
emT3XDataServer/Source/__recovery/ emT3XDataServer/Source/__recovery/
emT3XDataServer/Win32/ emT3XDataServer/Win32/
.validation/
*.exe *.exe
*.local *.local
......
...@@ -22,7 +22,7 @@ type ...@@ -22,7 +22,7 @@ type
public public
currentTaskId: string; currentTaskId: string;
currentTimeEntryDate: string; currentTimeEntryDate: string;
const clientVersion = '0.9.3'; const clientVersion = '0.9.4.0';
procedure InitApp(SuccessProc: TSuccessProc; procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc); UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure SetClientConfig(Callback: TVersionCheckCallback); procedure SetClientConfig(Callback: TVersionCheckCallback);
......
...@@ -58,6 +58,19 @@ object FTaskItems: TFTaskItems ...@@ -58,6 +58,19 @@ object FTaskItems: TFTaskItems
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnInsertRowClick OnClick = btnInsertRowClick
end end
object btnCopyRow: TWebButton
Left = 78
Top = 210
Width = 96
Height = 25
Caption = 'Copy Row'
Enabled = False
ElementID = 'btn_copy_row'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnCopyRowClick
end
object btnTextColor: TWebButton object btnTextColor: TWebButton
Left = 282 Left = 282
Top = 197 Top = 197
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<button id="btn_add_row" class="btn btn-sm btn-success text-nowrap">Add Row</button> <button id="btn_add_row" class="btn btn-sm btn-success text-nowrap">Add Row</button>
<button id="btn_insert_row" class="btn btn-sm btn-success text-nowrap">Insert Row</button> <button id="btn_insert_row" class="btn btn-sm btn-success text-nowrap">Insert Row</button>
<button id="btn_copy_row" class="btn btn-sm btn-primary text-nowrap" disabled>Copy Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-danger text-nowrap">Delete Row</button> <button id="btn_delete_row" class="btn btn-sm btn-danger text-nowrap">Delete Row</button>
<button id="btn_reload" class="btn btn-sm btn-primary text-nowrap">Reload</button> <button id="btn_reload" class="btn btn-sm btn-primary text-nowrap">Reload</button>
</div> </div>
......
...@@ -31,6 +31,7 @@ type ...@@ -31,6 +31,7 @@ type
xdwdsTaskstaskItemId: TIntegerField; xdwdsTaskstaskItemId: TIntegerField;
btnDeleteRow: TWebButton; btnDeleteRow: TWebButton;
btnInsertRow: TWebButton; btnInsertRow: TWebButton;
btnCopyRow: TWebButton;
btnTextColor: TWebButton; btnTextColor: TWebButton;
btnFmtBold: TWebButton; btnFmtBold: TWebButton;
btnFmtItalic: TWebButton; btnFmtItalic: TWebButton;
...@@ -40,6 +41,7 @@ type ...@@ -40,6 +41,7 @@ type
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
[async] procedure btnDeleteRowClick(Sender: TObject); [async] procedure btnDeleteRowClick(Sender: TObject);
[async] procedure btnInsertRowClick(Sender: TObject); [async] procedure btnInsertRowClick(Sender: TObject);
[async] procedure btnCopyRowClick(Sender: TObject);
procedure btnTextColorClick(Sender: TObject); procedure btnTextColorClick(Sender: TObject);
procedure btnFmtBoldClick(Sender: TObject); procedure btnFmtBoldClick(Sender: TObject);
procedure btnFmtItalicClick(Sender: TObject); procedure btnFmtItalicClick(Sender: TObject);
...@@ -61,6 +63,8 @@ type ...@@ -61,6 +63,8 @@ type
FActiveRichEditorId: string; FActiveRichEditorId: string;
FLastSelectionRange: JSValue; FLastSelectionRange: JSValue;
FNameManager: TNameManager; FNameManager: TNameManager;
FCopyingRow: Boolean;
[async] function CopyTaskRow: Boolean;
[async] procedure LoadTasks(const ATaskId: string); [async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable; procedure RenderTable;
procedure BindTableEditors; procedure BindTableEditors;
...@@ -134,6 +138,8 @@ begin ...@@ -134,6 +138,8 @@ begin
FStatusOptions := TJSArray.new; FStatusOptions := TJSArray.new;
FSelectedTaskItemId := 0; FSelectedTaskItemId := 0;
FSelectedTaskId := 0; FSelectedTaskId := 0;
FCopyingRow := False;
btnCopyRow.Enabled := False;
FNameManager := TNameManager.Create( FNameManager := TNameManager.Create(
function(const AFieldName: string): TJSArray function(const AFieldName: string): TJSArray
...@@ -465,6 +471,53 @@ begin ...@@ -465,6 +471,53 @@ begin
end; end;
end; end;
[async] procedure TFTaskItems.btnCopyRowClick(Sender: TObject);
begin
if FCopyingRow or (FSelectedTaskItemId <= 0) then
Exit;
FCopyingRow := True;
btnCopyRow.Enabled := False;
Utils.ShowSpinner('spinner');
try
if await(CopyTaskRow) then
begin
CaptureTableScroll;
await(LoadTasks(FTaskId));
end;
finally
FCopyingRow := False;
btnCopyRow.Enabled := FSelectedTaskItemId > 0;
Utils.HideSpinner('spinner');
end;
end;
[async] function TFTaskItems.CopyTaskRow: Boolean;
var
response: TXDataClientResponse;
sourceTaskItemId: Integer;
begin
Result := False;
sourceTaskItemId := FSelectedTaskItemId;
try
response := await(xdwcTasks.RawInvokeAsync(
'ITaskItemService.CopyTaskRow', [FTaskId, sourceTaskItemId]));
console.log('CopyTaskRow: response=' + string(TJSJSON.stringify(response.Result)));
FSelectedTaskItemId := StrToIntDef(string(TJSObject(response.Result)['value']), 0);
FPendingFocusTaskItemId := FSelectedTaskItemId;
FPendingFocusField := 'application';
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('CopyTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTaskItems.RowClick(Event: TJSEvent); procedure TFTaskItems.RowClick(Event: TJSEvent);
var var
rowEl: TJSHTMLElement; rowEl: TJSHTMLElement;
...@@ -488,6 +541,7 @@ end; ...@@ -488,6 +541,7 @@ end;
procedure TFTaskItems.ApplySelectedRowState; procedure TFTaskItems.ApplySelectedRowState;
begin begin
btnCopyRow.Enabled := (FSelectedTaskItemId > 0) and not FCopyingRow;
asm asm
const selectedTaskItemId = this.FSelectedTaskItemId; const selectedTaskItemId = this.FSelectedTaskItemId;
document.querySelectorAll('.task-row-selectable').forEach(function(row){ document.querySelectorAll('.task-row-selectable').forEach(function(row){
......
...@@ -99,10 +99,10 @@ object FTimeEntries: TFTimeEntries ...@@ -99,10 +99,10 @@ object FTimeEntries: TFTimeEntries
OnClick = btnSearchRangeClick OnClick = btnSearchRangeClick
end end
object btnCopyEntry: TWebButton object btnCopyEntry: TWebButton
Left = 440 Left = 88
Top = 122 Top = 152
Width = 96 Width = 79
Height = 25 Height = 31
Caption = 'Copy' Caption = 'Copy'
ChildOrder = 7 ChildOrder = 7
ElementID = 'btn_copy_entry' ElementID = 'btn_copy_entry'
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<h5 class="mb-0 text-nowrap" id="lbl_time_entries_title"></h5> <h5 class="mb-0 text-nowrap" id="lbl_time_entries_title"></h5>
<div class="d-flex align-items-center gap-2 ms-auto flex-nowrap"> <div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
<div id="lbl_time_total_hours" class="me-2 text-nowrap fw-semibold">Total Hours: 0</div>
<div id="lbl_time_total_rows" class="me-2 text-nowrap"></div> <div id="lbl_time_total_rows" class="me-2 text-nowrap"></div>
<div class="d-flex align-items-center gap-2 flex-nowrap"> <div class="d-flex align-items-center gap-2 flex-nowrap">
...@@ -72,12 +73,24 @@ ...@@ -72,12 +73,24 @@
<div id="task_picker_status" class="alert alert-danger py-1 px-2 mb-3 d-none small"></div> <div id="task_picker_status" class="alert alert-danger py-1 px-2 mb-3 d-none small"></div>
<div class="d-flex justify-content-end gap-2"> <div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-center gap-2 me-auto">
<input class="form-check-input m-0" type="checkbox" id="task_picker_add_focus" />
<label class="form-check-label mb-0" for="task_picker_add_focus">
Focused Task
</label>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas"> <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas">
Cancel Cancel
</button> </button>
<button type="button" class="btn btn-primary btn-sm" id="btn_task_picker_save" disabled>
Save <button type="button" class="btn btn-outline-success btn-sm" id="btn_task_picker_save" disabled>
Save
</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_task_picker_select" disabled>
Select
</button> </button>
</div> </div>
</div> </div>
......
...@@ -55,6 +55,8 @@ type ...@@ -55,6 +55,8 @@ type
FTaskPickerOffCanvas: TTaskPickerOffCanvas; FTaskPickerOffCanvas: TTaskPickerOffCanvas;
FAddingNewEntry: Boolean; FAddingNewEntry: Boolean;
FNewEntryRowIndex: Integer; FNewEntryRowIndex: Integer;
FEditingEntryId: Integer;
FOriginalTaskDate: string;
[async] procedure LoadTimeEntries; [async] procedure LoadTimeEntries;
function BeginAddEntry: Boolean; function BeginAddEntry: Boolean;
...@@ -63,6 +65,8 @@ type ...@@ -63,6 +65,8 @@ type
procedure BindNewEntryButtons; procedure BindNewEntryButtons;
procedure NewEntrySaveClick(Event: TJSEvent); procedure NewEntrySaveClick(Event: TJSEvent);
procedure NewEntryCancelClick(Event: TJSEvent); procedure NewEntryCancelClick(Event: TJSEvent);
[async] procedure EditedDateSaveClick(Event: TJSEvent);
procedure EditedDateCancelClick(Event: TJSEvent);
[async] procedure SaveNewEntry; [async] procedure SaveNewEntry;
procedure CancelNewEntry; procedure CancelNewEntry;
procedure RenderTable; procedure RenderTable;
...@@ -76,6 +80,7 @@ type ...@@ -76,6 +80,7 @@ type
function DateToIso(const AValue: TDateTime): string; function DateToIso(const AValue: TDateTime): string;
procedure SetWeekRangeFromAnchor(const ADateValue: string; ALoadAfterSet: Boolean); procedure SetWeekRangeFromAnchor(const ADateValue: string; ALoadAfterSet: Boolean);
procedure SetTotalRowsLabel(ARowCount: Integer); procedure SetTotalRowsLabel(ARowCount: Integer);
procedure UpdateTotalHours;
procedure SetTimeEntriesLabel(const AName: string); procedure SetTimeEntriesLabel(const AName: string);
procedure CaptureTableScroll; procedure CaptureTableScroll;
procedure RestoreTableScroll; procedure RestoreTableScroll;
...@@ -94,9 +99,10 @@ type ...@@ -94,9 +99,10 @@ type
procedure DocumentMouseDown(Event: TJSEvent); procedure DocumentMouseDown(Event: TJSEvent);
[async] procedure TaskPickerButtonClick(Event: TJSEvent); [async] procedure TaskPickerButtonClick(Event: TJSEvent);
procedure TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string); procedure TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string);
procedure TaskPickerFocusChanged(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
procedure ApplyActiveRowState; procedure ApplyActiveRowState;
procedure EditorBlur(Event: TJSEvent); procedure EditorBlur(Event: TJSEvent);
[async] procedure SaveField(AIndex: Integer; const AFieldName: string); [async] function SaveField(AIndex: Integer; const AFieldName: string): Boolean;
[async] procedure DeleteSelectedEntry; [async] procedure DeleteSelectedEntry;
function CopySelectedEntry: Boolean; function CopySelectedEntry: Boolean;
public public
...@@ -134,7 +140,7 @@ begin ...@@ -134,7 +140,7 @@ begin
FAddingNewEntry := False; FAddingNewEntry := False;
FNewEntryRowIndex := -1; FNewEntryRowIndex := -1;
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected); FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected, @TaskPickerFocusChanged);
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown)); document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
...@@ -215,7 +221,7 @@ var ...@@ -215,7 +221,7 @@ var
weekEnd: TDateTime; weekEnd: TDateTime;
begin begin
anchorDate := IsoToDate(ADateValue); anchorDate := IsoToDate(ADateValue);
weekStart := anchorDate - (DayOfTheWeek(anchorDate) - 1); weekStart := anchorDate;
weekEnd := weekStart + 6; weekEnd := weekStart + 6;
FUpdatingDates := True; FUpdatingDates := True;
...@@ -271,6 +277,37 @@ begin ...@@ -271,6 +277,37 @@ begin
el.innerText := 'Total Rows: ' + IntToStr(ARowCount); el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
end; end;
procedure TFTimeEntries.UpdateTotalHours;
var
el: TJSHTMLElement;
totalHours: Double;
currentRecNo: Integer;
begin
totalHours := 0;
if xdwdsTimeEntries.Active then
begin
currentRecNo := xdwdsTimeEntries.RecNo;
xdwdsTimeEntries.First;
while not xdwdsTimeEntries.Eof do
begin
if (xdwdsTimeEntriesentryId.AsInteger > 0) and
(xdwdsTimeEntriestaskDate.AsString >= FStartDate) and
(xdwdsTimeEntriestaskDate.AsString <= FEndDate) and
(not xdwdsTimeEntrieshours.IsNull) then
totalHours := totalHours + xdwdsTimeEntrieshours.AsFloat;
xdwdsTimeEntries.Next;
end;
if currentRecNo > 0 then
xdwdsTimeEntries.RecNo := currentRecNo;
end;
el := TJSHTMLElement(document.getElementById('lbl_time_total_hours'));
el.innerText := 'Total Hours: ' + FormatFloat('0.##', totalHours);
end;
procedure TFTimeEntries.SetTimeEntriesLabel(const AName: string); procedure TFTimeEntries.SetTimeEntriesLabel(const AName: string);
var var
el: TJSHTMLElement; el: TJSHTMLElement;
...@@ -284,6 +321,7 @@ begin ...@@ -284,6 +321,7 @@ begin
el.innerText := displayName + '''s Time Entries'; el.innerText := displayName + '''s Time Entries';
end; end;
[async] procedure TFTimeEntries.LoadTimeEntries; [async] procedure TFTimeEntries.LoadTimeEntries;
var var
response: TXDataClientResponse; response: TXDataClientResponse;
...@@ -333,6 +371,7 @@ begin ...@@ -333,6 +371,7 @@ begin
xdwdsTimeEntries.Close; xdwdsTimeEntries.Close;
xdwdsTimeEntries.SetJsonData(itemsArray); xdwdsTimeEntries.SetJsonData(itemsArray);
xdwdsTimeEntries.Open; xdwdsTimeEntries.Open;
UpdateTotalHours;
FActiveRowIndex := -1; FActiveRowIndex := -1;
HideRowValidationMessage; HideRowValidationMessage;
...@@ -347,8 +386,6 @@ begin ...@@ -347,8 +386,6 @@ begin
end; end;
procedure TFTimeEntries.RenderTable; procedure TFTimeEntries.RenderTable;
var var
host: TJSHTMLElement; host: TJSHTMLElement;
...@@ -459,8 +496,8 @@ var ...@@ -459,8 +496,8 @@ var
if AShowTaskPicker then if AShowTaskPicker then
begin begin
Result := Result + Result := Result +
'<button type="button" class="btn btn-outline-secondary time-task-picker-btn flex-shrink-0 text-nowrap" ' + '<button type="button" class="btn btn-outline-secondary time-task-picker-btn flex-shrink-0 text-nowrap ms-0" ' +
'data-idx="' + IntToStr(AIdx) + '" title="Find task">Find</button>' + 'data-idx="' + IntToStr(AIdx) + '" title="Select a task">Task</button>' +
'</div>'; '</div>';
end; end;
end; end;
...@@ -532,7 +569,15 @@ begin ...@@ -532,7 +569,15 @@ begin
'</div>' + '</div>' +
'</td>' + '</td>' +
'</tr>'; '</tr>';
end; end
else
html := html +
'<tr id="time-edit-actions-' + IntToStr(rowIdx) + '" hidden>' +
'<td colspan="7">' +
'<button type="button" class="btn btn-sm btn-success time-edit-save" data-idx="' + IntToStr(rowIdx) + '">Save</button> ' +
'<button type="button" class="btn btn-sm btn-outline-secondary time-edit-cancel" data-idx="' + IntToStr(rowIdx) + '">Cancel</button>' +
'</td>' +
'</tr>';
xdwdsTimeEntries.Next; xdwdsTimeEntries.Next;
Inc(rowIdx); Inc(rowIdx);
...@@ -683,6 +728,98 @@ begin ...@@ -683,6 +728,98 @@ begin
RenderTable; RenderTable;
end; end;
[async] procedure TFTimeEntries.EditedDateSaveClick(Event: TJSEvent);
var
saveButton, cancelButton: TJSHTMLButtonElement;
dateInput: TJSHTMLInputElement;
rowEl: TJSHTMLElement;
idx, savedEntryId: Integer;
rowSelector: string;
begin
saveButton := TJSHTMLButtonElement(Event.target);
idx := StrToIntDef(string(saveButton.getAttribute('data-idx')), -1);
if (idx < 0) or (FEditingEntryId = 0) or
(not xdwdsTimeEntries.Active) then
Exit;
GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then
Exit;
savedEntryId := xdwdsTimeEntriesentryId.AsInteger;
if savedEntryId <> FEditingEntryId then
Exit;
if Trim(xdwdsTimeEntriestaskDate.AsString) = '' then
begin
Utils.ShowErrorModal('Enter a date before saving.');
Exit;
end;
rowSelector := '.time-row-selectable[data-entry-id="' +
IntToStr(savedEntryId) + '"]';
rowEl := TJSHTMLElement(document.querySelector(rowSelector));
dateInput := TJSHTMLInputElement(rowEl.querySelector('[data-field="taskDate"]'));
cancelButton := TJSHTMLButtonElement(document.querySelector(
'.time-edit-cancel[data-idx="' + IntToStr(idx) + '"]'));
saveButton.disabled := True;
cancelButton.disabled := True;
dateInput.disabled := True;
try
if not await(SaveField(idx, 'taskDate')) then
Exit;
finally
saveButton.disabled := False;
cancelButton.disabled := False;
dateInput.disabled := False;
end;
FEditingEntryId := 0;
FOriginalTaskDate := '';
CaptureTableScroll;
RenderTable;
rowEl := TJSHTMLElement(document.querySelector(rowSelector));
if Assigned(rowEl) then
rowEl.classList.add('time-row-saved');
end;
procedure TFTimeEntries.EditedDateCancelClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
begin
Event.preventDefault;
Event.stopPropagation;
el := TJSHTMLElement(Event.target);
idx := StrToIntDef(string(el.getAttribute('data-idx')), -1);
if (idx < 0) or (FEditingEntryId = 0) or
(not xdwdsTimeEntries.Active) then
Exit;
GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then
Exit;
if xdwdsTimeEntriesentryId.AsInteger <> FEditingEntryId then
Exit;
xdwdsTimeEntries.Edit;
xdwdsTimeEntriestaskDate.AsString := FOriginalTaskDate;
xdwdsTimeEntries.Post;
FEditingEntryId := 0;
FOriginalTaskDate := '';
CaptureTableScroll;
RenderTable;
if ValidateRow(idx) then
ClearRowValidation(idx);
end;
procedure TFTimeEntries.BindTableEditors; procedure TFTimeEntries.BindTableEditors;
var var
nodes: TJSNodeList; nodes: TJSNodeList;
...@@ -700,6 +837,22 @@ begin ...@@ -700,6 +837,22 @@ begin
el.addEventListener('blur', TJSEventHandler(@EditorBlur)); el.addEventListener('blur', TJSEventHandler(@EditorBlur));
end; end;
nodes := document.querySelectorAll('.time-edit-cancel');
console.log('BindTableEditors: edit row cancel button clicked');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@EditedDateCancelClick));
end;
nodes := document.querySelectorAll('.time-edit-save');
console.log('BindTableEditors: edit row save button click');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@EditedDateSaveClick));
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
...@@ -734,6 +887,7 @@ var ...@@ -734,6 +887,7 @@ var
idxStr: string; idxStr: string;
fieldName: string; fieldName: string;
newVal: string; newVal: string;
actionsRow: TJSHTMLElement;
begin begin
if not xdwdsTimeEntries.Active then if not xdwdsTimeEntries.Active then
Exit; Exit;
...@@ -755,6 +909,14 @@ begin ...@@ -755,6 +909,14 @@ begin
if xdwdsTimeEntries.Eof then if xdwdsTimeEntries.Eof then
Exit; Exit;
if SameText(fieldName, 'taskDate') and
(xdwdsTimeEntriesentryId.AsInteger > 0) and
(FEditingEntryId = 0) then
begin
FEditingEntryId := xdwdsTimeEntriesentryId.AsInteger;
FOriginalTaskDate := xdwdsTimeEntriestaskDate.AsString;
end;
newVal := string(TJSObject(el)['value']); newVal := string(TJSObject(el)['value']);
xdwdsTimeEntries.Edit; xdwdsTimeEntries.Edit;
...@@ -777,6 +939,15 @@ begin ...@@ -777,6 +939,15 @@ begin
xdwdsTimeEntries.Post; xdwdsTimeEntries.Post;
if SameText(fieldName, 'taskDate') and (FEditingEntryId > 0) and
(xdwdsTimeEntriesentryId.AsInteger = FEditingEntryId) then
begin
actionsRow := TJSHTMLElement(
document.getElementById('time-edit-actions-' + IntToStr(idx)));
if Assigned(actionsRow) then
actionsRow.removeAttribute('hidden');
end;
el.setAttribute('data-unsaved-data', '1'); el.setAttribute('data-unsaved-data', '1');
if IsNewEntryRow(idx) then if IsNewEntryRow(idx) then
...@@ -1616,6 +1787,7 @@ end; ...@@ -1616,6 +1787,7 @@ end;
var var
el: TJSHTMLElement; el: TJSHTMLElement;
idx: Integer; idx: Integer;
taskId: string;
begin begin
Event.preventDefault; Event.preventDefault;
Event.stopPropagation; Event.stopPropagation;
...@@ -1665,9 +1837,63 @@ begin ...@@ -1665,9 +1837,63 @@ begin
btnCopyEntry.Enabled := not IsNewEntryRow(idx); btnCopyEntry.Enabled := not IsNewEntryRow(idx);
if not Assigned(FTaskPickerOffCanvas) then if not Assigned(FTaskPickerOffCanvas) then
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected); FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected, @TaskPickerFocusChanged);
// Pass the row's current task so the picker opens on it (blank for rows without one)
taskId := '';
GotoRowIndex(idx);
if not xdwdsTimeEntries.Eof then
taskId := xdwdsTimeEntriestaskId.AsString;
await(FTaskPickerOffCanvas.Open(FUserId, idx)); await(FTaskPickerOffCanvas.Open(FUserId, idx, FTaskOptions, taskId));
end;
procedure TFTimeEntries.TaskPickerFocusChanged(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
var
i: Integer;
taskIndex: Integer;
taskOption: TJSObject;
begin
if not Assigned(FTaskOptions) then
FTaskOptions := TJSArray.new;
taskIndex := -1;
for i := 0 to FTaskOptions.length - 1 do
begin
taskOption := TJSObject(FTaskOptions[i]);
if string(taskOption['taskId']) = ATaskId then
begin
taskIndex := i;
Break;
end;
end;
if AFocused then
begin
if taskIndex >= 0 then
Exit;
taskOption := TJSObject.new;
taskOption['taskId'] := ATaskId;
taskOption['taskDisplay'] := ATaskDisplay;
FTaskOptions.push(taskOption);
end
else
begin
if taskIndex < 0 then
Exit;
// Rows already using this task keep it; it is only removed from the dropdown options
FTaskOptions.splice(taskIndex, 1);
end;
if xdwdsTimeEntries.Active then
begin
CaptureTableScroll;
RenderTable;
end;
end; end;
...@@ -1773,17 +1999,27 @@ begin ...@@ -1773,17 +1999,27 @@ begin
Exit; Exit;
end; end;
if SameText(fieldName, 'taskDate') and (FEditingEntryId > 0) then
begin
GotoRowIndex(idx);
if not xdwdsTimeEntries.Eof then
if xdwdsTimeEntriesentryId.AsInteger = FEditingEntryId then
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;
[async] procedure TFTimeEntries.SaveField(AIndex: Integer; const AFieldName: string); [async] function TFTimeEntries.SaveField(AIndex: Integer; const AFieldName: string): Boolean;
var var
response: TXDataClientResponse; response: TXDataClientResponse;
payload: TJSObject; payload: TJSObject;
fieldValue: string; fieldValue: string;
begin begin
Result := False;
if not xdwdsTimeEntries.Active then if not xdwdsTimeEntries.Active then
Exit; Exit;
...@@ -1831,8 +2067,11 @@ begin ...@@ -1831,8 +2067,11 @@ begin
'ITimeEntryService.SaveTimeEntryField', 'ITimeEntryService.SaveTimeEntryField',
[payload] [payload]
)); ));
Result := True;
console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result))); console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result)));
if SameText(AFieldName, 'hours') or SameText(AFieldName, 'taskDate') then
UpdateTotalHours;
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
begin begin
......
...@@ -49,4 +49,18 @@ ...@@ -49,4 +49,18 @@
.time-dd-toggle.is-invalid { .time-dd-toggle.is-invalid {
border-color: var(--bs-danger) !important; border-color: var(--bs-danger) !important;
padding-right: calc(1.5em + .75rem); padding-right: calc(1.5em + .75rem);
}
.time-row-saved > td {
animation: time-row-saved-flash 900ms ease-out;
}
@keyframes time-row-saved-flash {
from {
box-shadow: inset 0 0 0 999px rgba(25, 135, 84, 0.25);
}
to {
box-shadow: inset 0 0 0 999px rgba(25, 135, 84, 0);
}
} }
\ No newline at end of file
...@@ -94,15 +94,15 @@ ...@@ -94,15 +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.9.3.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.4.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>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>4</VerInfo_Release>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSUseJSDebugger>2</TMSUseJSDebugger>
<VerInfo_Release>3</VerInfo_Release>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<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>
......
...@@ -6,7 +6,15 @@ uses ...@@ -6,7 +6,15 @@ uses
System.SysUtils, JS, Web, XData.Web.Client; System.SysUtils, JS, Web, XData.Web.Client;
type type
TTaskPickedProc = reference to procedure(ARowIndex: Integer; const ATaskId, ATaskDisplay: string); TTaskPickedProc = reference to procedure(
ARowIndex: Integer;
const ATaskId, ATaskDisplay: string
);
TTaskFocusChangedProc = reference to procedure(
const ATaskId, ATaskDisplay: string;
AFocused: Boolean
);
TTaskPickerOffCanvas = class TTaskPickerOffCanvas = class
private private
...@@ -14,45 +22,68 @@ type ...@@ -14,45 +22,68 @@ type
FUserId: string; FUserId: string;
FRowIndex: Integer; FRowIndex: Integer;
FOnTaskPicked: TTaskPickedProc; FOnTaskPicked: TTaskPickedProc;
FOnFocusChanged: TTaskFocusChangedProc;
FCustomers: TJSArray; FCustomers: TJSArray;
FProjects: TJSArray; FProjects: TJSArray;
FTasks: TJSArray; FTasks: TJSArray;
FFocusedTasks: TJSArray;
FSavedFocus: Boolean;
FSaving: Boolean;
FPrefillToken: Integer;
procedure BindEvents; procedure BindEvents;
procedure ClearSelect(const AElementId: string; ADisabled: Boolean); procedure ClearSelect(const AElementId: string; ADisabled: Boolean);
procedure FillSelect(const AElementId: string; AItems: TJSArray); procedure FillSelect(const AElementId: string; AItems: TJSArray);
procedure FilterProjects(const ACustomerId: string);
function SelectedValue(const AElementId: string): string; function SelectedValue(const AElementId: string): string;
function FindDisplay(AItems: TJSArray; const AValue: string): string; function FindDisplay(AItems: TJSArray; const AValue: string): string;
function FocusCheckbox: TJSHTMLInputElement;
function IsTaskFocused(const ATaskId: string): Boolean;
procedure SetTaskFocused(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
procedure SetStatus(const AMessage: string); procedure SetStatus(const AMessage: string);
procedure ClearStatus; procedure ClearStatus;
procedure SetSaveEnabled(AEnabled: Boolean); procedure SetButtonEnabled(const AElementId: string; AEnabled: Boolean);
procedure UpdateButtons;
procedure Show; procedure Show;
procedure Hide; procedure Hide;
[async] procedure LoadCustomers; [async] procedure LoadCustomers;
[async] procedure LoadProjects(const ACustomerId: string); [async] procedure LoadProjects(const ACustomerId: string);
[async] procedure LoadTasks(const AProjectId: string); [async] procedure LoadTasks(const AProjectId: string);
[async] procedure PreselectTask(const ATaskId: string; AToken: Integer);
procedure SetSelectValue(const AElementId, AValue: string);
[async] procedure CustomerChanged(Event: TJSEvent); procedure CustomerChanged(Event: TJSEvent);
[async] procedure ProjectChanged(Event: TJSEvent); [async] procedure ProjectChanged(Event: TJSEvent);
procedure TaskChanged(Event: TJSEvent); procedure TaskChanged(Event: TJSEvent);
procedure SaveClicked(Event: TJSEvent); procedure FocusCheckboxChanged(Event: TJSEvent);
procedure UpdateFocusCheckbox;
procedure SelectClicked(Event: TJSEvent);
[async] procedure SaveClicked(Event: TJSEvent);
public public
constructor Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc); constructor Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc;
[async] procedure Open(const AUserId: string; ARowIndex: Integer); AOnFocusChanged: TTaskFocusChangedProc);
[async] procedure Open(const AUserId: string; ARowIndex: Integer; AFocusedTasks: TJSArray;
const ATaskId: string);
end; end;
implementation implementation
constructor TTaskPickerOffCanvas.Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc); constructor TTaskPickerOffCanvas.Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc;
AOnFocusChanged: TTaskFocusChangedProc);
begin begin
inherited Create; inherited Create;
FClient := AClient; FClient := AClient;
FOnTaskPicked := AOnTaskPicked; FOnTaskPicked := AOnTaskPicked;
FOnFocusChanged := AOnFocusChanged;
FCustomers := TJSArray.new; FCustomers := TJSArray.new;
FProjects := TJSArray.new; FProjects := TJSArray.new;
FTasks := TJSArray.new; FTasks := TJSArray.new;
FFocusedTasks := TJSArray.new;
FRowIndex := -1; FRowIndex := -1;
FSavedFocus := False;
FSaving := False;
FPrefillToken := 0;
BindEvents; BindEvents;
end; end;
...@@ -73,9 +104,17 @@ begin ...@@ -73,9 +104,17 @@ begin
if Assigned(el) then if Assigned(el) then
el.addEventListener('change', TJSEventHandler(@TaskChanged)); el.addEventListener('change', TJSEventHandler(@TaskChanged));
el := TJSHTMLElement(document.getElementById('task_picker_add_focus'));
if Assigned(el) then
el.addEventListener('change', TJSEventHandler(@FocusCheckboxChanged));
el := TJSHTMLElement(document.getElementById('btn_task_picker_save')); el := TJSHTMLElement(document.getElementById('btn_task_picker_save'));
if Assigned(el) then if Assigned(el) then
el.addEventListener('click', TJSEventHandler(@SaveClicked)); el.addEventListener('click', TJSEventHandler(@SaveClicked));
el := TJSHTMLElement(document.getElementById('btn_task_picker_select'));
if Assigned(el) then
el.addEventListener('click', TJSEventHandler(@SelectClicked));
end; end;
procedure TTaskPickerOffCanvas.ClearSelect(const AElementId: string; ADisabled: Boolean); procedure TTaskPickerOffCanvas.ClearSelect(const AElementId: string; ADisabled: Boolean);
...@@ -115,6 +154,28 @@ begin ...@@ -115,6 +154,28 @@ begin
selectEl.disabled := False; selectEl.disabled := False;
end; end;
procedure TTaskPickerOffCanvas.FilterProjects(const ACustomerId: string);
var
filteredProjects: TJSArray;
project: TJSObject;
i: Integer;
begin
//create an empty filtered project list
filteredProjects := TJSArray.new;
//loop through the full project list
for i := 0 to FProjects.length -1 do
begin
project := TJSObject(FProjects[i]);
if (ACustomerId = '') or (string(project['customerId']) = ACustomerId) then
begin
filteredProjects.push(project);
end;
end;
FillSelect('task_picker_project', filteredProjects)
end;
function TTaskPickerOffCanvas.SelectedValue(const AElementId: string): string; function TTaskPickerOffCanvas.SelectedValue(const AElementId: string): string;
var var
selectEl: TJSHTMLSelectElement; selectEl: TJSHTMLSelectElement;
...@@ -141,6 +202,58 @@ begin ...@@ -141,6 +202,58 @@ begin
end; end;
end; end;
function TTaskPickerOffCanvas.FocusCheckbox: TJSHTMLInputElement;
begin
Result := TJSHTMLInputElement(document.getElementById('task_picker_add_focus'));
end;
function TTaskPickerOffCanvas.IsTaskFocused(const ATaskId: string): Boolean;
var
taskOption: TJSObject;
i: Integer;
begin
Result := False;
if ATaskId = '' then
Exit;
for i := 0 to FFocusedTasks.length - 1 do
begin
taskOption := TJSObject(FFocusedTasks[i]);
if string(taskOption['taskId']) = ATaskId then
Exit(True);
end;
end;
procedure TTaskPickerOffCanvas.SetTaskFocused(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
var
taskOption: TJSObject;
i: Integer;
begin
// Keep the picker's own copy of the focused list in sync with the server
for i := FFocusedTasks.length - 1 downto 0 do
begin
taskOption := TJSObject(FFocusedTasks[i]);
if string(taskOption['taskId']) = ATaskId then
begin
if AFocused then
Exit;
FFocusedTasks.splice(i, 1);
end;
end;
if AFocused then
begin
taskOption := TJSObject.new;
taskOption['taskId'] := ATaskId;
taskOption['taskDisplay'] := ATaskDisplay;
FFocusedTasks.push(taskOption);
end;
end;
procedure TTaskPickerOffCanvas.SetStatus(const AMessage: string); procedure TTaskPickerOffCanvas.SetStatus(const AMessage: string);
var var
el: TJSHTMLElement; el: TJSHTMLElement;
...@@ -165,15 +278,38 @@ begin ...@@ -165,15 +278,38 @@ begin
el.classList.add('d-none'); el.classList.add('d-none');
end; end;
procedure TTaskPickerOffCanvas.SetSaveEnabled(AEnabled: Boolean); procedure TTaskPickerOffCanvas.SetButtonEnabled(const AElementId: string; AEnabled: Boolean);
var var
btn: TJSHTMLButtonElement; btn: TJSHTMLButtonElement;
begin begin
btn := TJSHTMLButtonElement(document.getElementById('btn_task_picker_save')); btn := TJSHTMLButtonElement(document.getElementById(AElementId));
if Assigned(btn) then if Assigned(btn) then
btn.disabled := not AEnabled; btn.disabled := not AEnabled;
end; end;
procedure TTaskPickerOffCanvas.UpdateButtons;
var
hasTask: Boolean;
focusChanged: Boolean;
addFocus: TJSHTMLInputElement;
begin
// Select: any picked task can be sent to the grid.
// Save: only when the Focused Task checkbox differs from what is saved on the server.
hasTask := SelectedValue('task_picker_task') <> '';
addFocus := FocusCheckbox;
focusChanged := False;
if Assigned(addFocus) then
begin
addFocus.disabled := (not hasTask) or FSaving;
focusChanged := addFocus.checked <> FSavedFocus;
end;
SetButtonEnabled('btn_task_picker_select', hasTask and not FSaving);
SetButtonEnabled('btn_task_picker_save', hasTask and focusChanged and not FSaving);
end;
procedure TTaskPickerOffCanvas.Show; procedure TTaskPickerOffCanvas.Show;
begin begin
asm asm
...@@ -202,8 +338,7 @@ var ...@@ -202,8 +338,7 @@ var
resultObj: TJSObject; resultObj: TJSObject;
begin begin
response := await(FClient.RawInvokeAsync( response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerCustomers', 'ITimeEntryService.GetTaskPickerCustomers', []
[FUserId]
)); ));
resultObj := TJSObject(response.Result); resultObj := TJSObject(response.Result);
...@@ -218,12 +353,11 @@ var ...@@ -218,12 +353,11 @@ var
begin begin
response := await(FClient.RawInvokeAsync( response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerProjects', 'ITimeEntryService.GetTaskPickerProjects',
[FUserId, ACustomerId] [ACustomerId]
)); ));
resultObj := TJSObject(response.Result); resultObj := TJSObject(response.Result);
FProjects := TJSArray(resultObj['options']); FProjects := TJSArray(resultObj['options']);
FillSelect('task_picker_project', FProjects);
end; end;
[async] procedure TTaskPickerOffCanvas.LoadTasks(const AProjectId: string); [async] procedure TTaskPickerOffCanvas.LoadTasks(const AProjectId: string);
...@@ -233,7 +367,7 @@ var ...@@ -233,7 +367,7 @@ var
begin begin
response := await(FClient.RawInvokeAsync( response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerTasks', 'ITimeEntryService.GetTaskPickerTasks',
[FUserId, AProjectId] [AProjectId]
)); ));
resultObj := TJSObject(response.Result); resultObj := TJSObject(response.Result);
...@@ -241,39 +375,121 @@ begin ...@@ -241,39 +375,121 @@ begin
FillSelect('task_picker_task', FTasks); FillSelect('task_picker_task', FTasks);
end; end;
[async] procedure TTaskPickerOffCanvas.CustomerChanged(Event: TJSEvent); procedure TTaskPickerOffCanvas.SetSelectValue(const AElementId, AValue: string);
var var
selectEl: TJSHTMLSelectElement;
begin
// Setting .value in code does not fire 'change', so the cascade handlers do not run
selectEl := TJSHTMLSelectElement(document.getElementById(AElementId));
if Assigned(selectEl) then
selectEl.value := AValue;
end;
[async] procedure TTaskPickerOffCanvas.PreselectTask(const ATaskId: string; AToken: Integer);
var
response: TXDataClientResponse;
context: TJSObject;
project: TJSObject;
projectId: string;
customerId: string; customerId: string;
projectFound: Boolean;
i: Integer;
begin begin
response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerTaskContext',
[ATaskId]
));
// Picker was reopened or the user changed a dropdown while this was loading
if AToken <> FPrefillToken then
Exit;
context := TJSObject(response.Result);
projectId := string(context['projectId']);
if projectId = '' then
Exit;
// FProjects only holds ACTIVE projects; a task under any other project opens blank
projectFound := False;
customerId := '';
for i := 0 to FProjects.length - 1 do
begin
project := TJSObject(FProjects[i]);
if string(project['value']) = projectId then
begin
customerId := string(project['customerId']);
projectFound := True;
Break;
end;
end;
if not projectFound then
Exit;
SetSelectValue('task_picker_customer', customerId);
FilterProjects(customerId);
SetSelectValue('task_picker_project', projectId);
await(LoadTasks(projectId));
if AToken <> FPrefillToken then
Exit;
// If the task is not in the list (e.g. moved project) the task select just stays blank
SetSelectValue('task_picker_task', ATaskId);
UpdateFocusCheckbox;
end;
procedure TTaskPickerOffCanvas.CustomerChanged(Event: TJSEvent);
var
customerId: string;
begin
Inc(FPrefillToken); // user took over; abandon any prefill still loading
ClearStatus; ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_project', True); ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True); ClearSelect('task_picker_task', True);
UpdateFocusCheckbox;
customerId := SelectedValue('task_picker_customer'); customerId := SelectedValue('task_picker_customer');
if customerId = '' then FilterProjects(customerId);
Exit;
try
await(LoadProjects(customerId));
except
on E: Exception do
SetStatus('Unable to load projects.');
end;
end; end;
[async] procedure TTaskPickerOffCanvas.ProjectChanged(Event: TJSEvent); [async] procedure TTaskPickerOffCanvas.ProjectChanged(Event: TJSEvent);
var var
projectId: string; projectId: string;
customerId: string;
project: TJSObject;
i: Integer;
begin begin
Inc(FPrefillToken); // user took over; abandon any prefill still loading
ClearStatus; ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_task', True); ClearSelect('task_picker_task', True);
UpdateFocusCheckbox;
projectId := SelectedValue('task_picker_project'); projectId := SelectedValue('task_picker_project');
if projectId = '' then if projectId = '' then
Exit; Exit;
customerId := SelectedValue('task_picker_customer');
if customerId = '' then
begin
for i := 0 to FProjects.length - 1 do
begin
project := TJSObject(FProjects[i]);
if string(project['value']) = projectId then
begin
customerId := string(project['customerId']);
Break;
end;
end;
TJSHTMLSelectElement(document.getElementById('task_picker_customer')).value := customerId;
FilterProjects(customerId);
TJSHTMLSelectElement(document.getElementById('task_picker_project')).value := projectId;
end;
try try
await(LoadTasks(projectId)); await(LoadTasks(projectId));
except except
...@@ -282,16 +498,36 @@ begin ...@@ -282,16 +498,36 @@ begin
end; end;
end; end;
procedure TTaskPickerOffCanvas.UpdateFocusCheckbox;
var
addFocus: TJSHTMLInputElement;
begin
// Reset the checkbox to the task's saved focus state (discards any unsaved toggle)
FSavedFocus := IsTaskFocused(SelectedValue('task_picker_task'));
addFocus := FocusCheckbox;
if Assigned(addFocus) then
addFocus.checked := FSavedFocus;
UpdateButtons;
end;
procedure TTaskPickerOffCanvas.TaskChanged(Event: TJSEvent); procedure TTaskPickerOffCanvas.TaskChanged(Event: TJSEvent);
begin begin
Inc(FPrefillToken); // user took over; abandon any prefill still loading
ClearStatus;
UpdateFocusCheckbox;
end;
procedure TTaskPickerOffCanvas.FocusCheckboxChanged(Event: TJSEvent);
begin
ClearStatus; ClearStatus;
SetSaveEnabled(SelectedValue('task_picker_task') <> ''); UpdateButtons;
end; end;
procedure TTaskPickerOffCanvas.SaveClicked(Event: TJSEvent); procedure TTaskPickerOffCanvas.SelectClicked(Event: TJSEvent);
var var
taskId: string; taskId: string;
taskDisplay: string;
begin begin
Event.preventDefault; Event.preventDefault;
...@@ -302,32 +538,136 @@ begin ...@@ -302,32 +538,136 @@ begin
Exit; Exit;
end; end;
taskDisplay := FindDisplay(FTasks, taskId); ClearStatus;
// Sends the task to the grid only; focus changes happen through Save
if Assigned(FOnTaskPicked) then if Assigned(FOnTaskPicked) then
FOnTaskPicked(FRowIndex, taskId, taskDisplay); FOnTaskPicked(FRowIndex, taskId, FindDisplay(FTasks, taskId));
Hide; Hide;
end; end;
[async] procedure TTaskPickerOffCanvas.Open(const AUserId: string; ARowIndex: Integer); [async] procedure TTaskPickerOffCanvas.SaveClicked(Event: TJSEvent);
var
taskId: string;
taskDisplay: string;
methodName: string;
addFocus: TJSHTMLInputElement;
focused: Boolean;
begin
Event.preventDefault;
if FSaving then
Exit;
taskId := SelectedValue('task_picker_task');
if taskId = '' then
begin
SetStatus('Select a task.');
Exit;
end;
addFocus := FocusCheckbox;
if not Assigned(addFocus) then
Exit;
focused := addFocus.checked;
if focused = FSavedFocus then
begin
UpdateButtons;
Exit;
end;
taskDisplay := FindDisplay(FTasks, taskId);
if focused then
methodName := 'ITimeEntryService.AddTaskToFocus'
else
methodName := 'ITimeEntryService.RemoveTaskFromFocus';
ClearStatus;
FSaving := True;
UpdateButtons;
try
try
await(FClient.RawInvokeAsync(methodName, [FUserId, taskId]));
except
on E: EXDataClientRequestException do
begin
SetStatus(E.ErrorResult.ErrorMessage);
Exit;
end;
on E: Exception do
begin
SetStatus('Unable to save focused task.');
Exit;
end;
end;
SetTaskFocused(taskId, taskDisplay, focused);
// The user may have picked a different task while the request was running
if SelectedValue('task_picker_task') = taskId then
FSavedFocus := focused;
if Assigned(FOnFocusChanged) then
FOnFocusChanged(taskId, taskDisplay, focused);
finally
FSaving := False;
UpdateButtons;
end;
end;
[async] procedure TTaskPickerOffCanvas.Open(
const AUserId: string;
ARowIndex: Integer;
AFocusedTasks: TJSArray;
const ATaskId: string
);
var
token: Integer;
begin begin
FUserId := AUserId; FUserId := AUserId;
FRowIndex := ARowIndex; FRowIndex := ARowIndex;
Inc(FPrefillToken);
token := FPrefillToken;
// Work on a copy so the grid's task options are only changed through FOnFocusChanged
if Assigned(AFocusedTasks) then
FFocusedTasks := AFocusedTasks.slice(0)
else
FFocusedTasks := TJSArray.new;
ClearStatus; ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_customer', True); ClearSelect('task_picker_customer', True);
ClearSelect('task_picker_project', True); ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True); ClearSelect('task_picker_task', True);
UpdateFocusCheckbox;
Show; Show;
try try
await(LoadCustomers); await(LoadCustomers);
await(LoadProjects(''));
FilterProjects('');
except
on E: Exception do
begin
SetStatus('Unable to load customers or projects.');
Exit;
end;
end;
// Row already has a task: open on its customer, project and task
if (ATaskId = '') or (token <> FPrefillToken) then
Exit;
try
await(PreselectTask(ATaskId, token));
except except
on E: Exception do on E: Exception do
SetStatus('Unable to load customers.'); SetStatus('Unable to load this row''s task.');
end; end;
end; end;
......
...@@ -7,10 +7,10 @@ object ApiDatabase: TApiDatabase ...@@ -7,10 +7,10 @@ object ApiDatabase: TApiDatabase
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.102.133' Server = '192.168.90.131'
LoginPrompt = False LoginPrompt = False
Left = 435 Left = 439
Top = 359 Top = 363
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF' EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end end
object MySQLUniProvider1: TMySQLUniProvider object MySQLUniProvider1: TMySQLUniProvider
...@@ -265,6 +265,60 @@ object ApiDatabase: TApiDatabase ...@@ -265,6 +265,60 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqCopyTaskItem: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert into task_items ('
' TASK_ID,'
' ITEM_NUM,'
' APPLICATION,'
' APP_VERSION,'
' TASK_DATE,'
' STATUS_DATE,'
' REPORTED_BY,'
' ASSIGNED_TO,'
' STATUS,'
' FIXED_VERSION,'
' FORM_SECTION,'
' ISSUE,'
' NOTES'
')'
'select'
' TASK_ID,'
' :ITEM_NUM,'
' APPLICATION,'
' APP_VERSION,'
' TASK_DATE,'
' STATUS_DATE,'
' REPORTED_BY,'
' ASSIGNED_TO,'
' STATUS,'
' FIXED_VERSION,'
' FORM_SECTION,'
' ISSUE,'
' NOTES'
'from task_items'
'where TASK_ID = :TASK_ID'
' and TASK_ITEM_ID = :TASK_ITEM_ID')
Left = 708
Top = 302
ParamData = <
item
DataType = ftInteger
Name = 'ITEM_NUM'
Value = nil
end
item
DataType = ftString
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftInteger
Name = 'TASK_ITEM_ID'
Value = nil
end>
end
object uqTaskHeader: TUniQuery object uqTaskHeader: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
SQL.Strings = ( SQL.Strings = (
...@@ -565,7 +619,8 @@ object ApiDatabase: TApiDatabase ...@@ -565,7 +619,8 @@ object ApiDatabase: TApiDatabase
'update task_items' 'update task_items'
'set ITEM_NUM = ITEM_NUM + 1' 'set ITEM_NUM = ITEM_NUM + 1'
'where TASK_ID = :TASK_ID' 'where TASK_ID = :TASK_ID'
' and ITEM_NUM > :INSERT_AFTER_ITEM_NUM') ' and ITEM_NUM > :INSERT_AFTER_ITEM_NUM'
'order by ITEM_NUM desc')
Left = 538 Left = 538
Top = 76 Top = 76
ParamData = < ParamData = <
...@@ -798,7 +853,7 @@ object ApiDatabase: TApiDatabase ...@@ -798,7 +853,7 @@ object ApiDatabase: TApiDatabase
'where target_task.TASK_ID = :TASK_ID' 'where target_task.TASK_ID = :TASK_ID'
'order by tia.NAME') 'order by tia.NAME')
Left = 58 Left = 58
Top = 314 Top = 316
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
...@@ -1224,50 +1279,47 @@ object ApiDatabase: TApiDatabase ...@@ -1224,50 +1279,47 @@ object ApiDatabase: TApiDatabase
SQL.Strings = ( SQL.Strings = (
'select distinct' 'select distinct'
' c.CUSTOMER_ID,' ' c.CUSTOMER_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME' ' c.NAME as CUSTOMER_NAME'
'from user_project up' 'from customers c '
'join project p on p.PROJECT_ID = up.PROJECT_ID' 'join project p on p.CUSTOMER_ID = c.CUSTOMER_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID' 'where p.STATUS = '#39'ACTIVE'#39
'where up.USER_ID = :USER_ID' 'order by c.NAME'
'order by c.SHORT_NAME') '')
Left = 402 Left = 404
Top = 494 Top = 494
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
object uqTaskPickerCustomersCUSTOMER_ID: TStringField object uqTaskPickerCustomersCUSTOMER_ID: TStringField
FieldName = 'CUSTOMER_ID' FieldName = 'CUSTOMER_ID'
Size = 7 Size = 7
end end
object uqTaskPickerCustomersCUSTOMER_SHORT_NAME: TStringField object uqTaskPickerCustomersCUSTOMER_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME' FieldName = 'CUSTOMER_NAME'
Size = 10 Size = 30
end end
end end
object uqTaskPickerProjects: TUniQuery object uqTaskPickerProjects: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
SQL.Strings = ( SQL.Strings = (
'select distinct' 'select'
' p.PROJECT_ID,' ' p.PROJECT_ID,'
' p.NAME as PROJECT_NAME' ' p.NAME as PROJECT_NAME,'
'from user_project up' ' p.CUSTOMER_ID'
'join project p on p.PROJECT_ID = up.PROJECT_ID' 'from project p '
'where up.USER_ID = :USER_ID' 'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
' and p.CUSTOMER_ID = :CUSTOMER_ID' 'where p.STATUS = '#39'ACTIVE'#39
'order by p.NAME') 'and (:CUSTOMER_ID = '#39#39' or p.CUSTOMER_ID = :CUSTOMER_ID)'
'order by p.NAME'
''
''
''
''
''
''
'')
Left = 544 Left = 544
Top = 494 Top = 494
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'CUSTOMER_ID' Name = 'CUSTOMER_ID'
Value = nil Value = nil
end> end>
...@@ -1280,13 +1332,17 @@ object ApiDatabase: TApiDatabase ...@@ -1280,13 +1332,17 @@ object ApiDatabase: TApiDatabase
FieldName = 'PROJECT_NAME' FieldName = 'PROJECT_NAME'
Size = 30 Size = 30
end end
object uqTaskPickerProjectsCUSTOMER_ID: TStringField
FieldName = 'CUSTOMER_ID'
Size = 7
end
end end
object uqTaskPickerTasks: TUniQuery object uqTaskPickerTasks: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
SQL.Strings = ( SQL.Strings = (
'select distinct' 'select distinct'
' t.TASK_ID,' ' t.TASK_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME,' ' c.SHORT_NAME as CUSTOMER_NAME,'
' p.NAME as PROJECT_NAME,' ' p.NAME as PROJECT_NAME,'
' t.TASK_NUM_1,' ' t.TASK_NUM_1,'
' t.TASK_NUM_2,' ' t.TASK_NUM_2,'
...@@ -1298,9 +1354,8 @@ object ApiDatabase: TApiDatabase ...@@ -1298,9 +1354,8 @@ object ApiDatabase: TApiDatabase
'from tasks t' 'from tasks t'
'join project p on p.PROJECT_ID = t.PROJECT_ID' 'join project p on p.PROJECT_ID = t.PROJECT_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID' 'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
'join user_project up on up.PROJECT_ID = p.PROJECT_ID' ' where t.PROJECT_ID = :PROJECT_ID'
'where up.USER_ID = :USER_ID' ' and p.STATUS = '#39'ACTIVE'#39
' and t.PROJECT_ID = :PROJECT_ID'
'order by' 'order by'
' t.TASK_NUM_1,' ' t.TASK_NUM_1,'
' t.TASK_NUM_2,' ' t.TASK_NUM_2,'
...@@ -1308,17 +1363,13 @@ object ApiDatabase: TApiDatabase ...@@ -1308,17 +1363,13 @@ object ApiDatabase: TApiDatabase
' t.TASK_NUM_4,' ' t.TASK_NUM_4,'
' t.TASK_NUM_5,' ' t.TASK_NUM_5,'
' t.TASK_NUM_6,' ' t.TASK_NUM_6,'
' t.SUBJECT') ' t.SUBJECT'
'')
Left = 676 Left = 676
Top = 502 Top = 502
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'PROJECT_ID' Name = 'PROJECT_ID'
Value = nil Value = nil
end> end>
...@@ -1327,10 +1378,10 @@ object ApiDatabase: TApiDatabase ...@@ -1327,10 +1378,10 @@ object ApiDatabase: TApiDatabase
Required = True Required = True
Size = 7 Size = 7
end end
object uqTaskPickerTasksCUSTOMER_SHORT_NAME: TStringField object uqTaskPickerTasksCUSTOMER_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME' FieldName = 'CUSTOMER_NAME'
ReadOnly = True ReadOnly = True
Size = 10 Size = 30
end end
object uqTaskPickerTasksPROJECT_NAME: TStringField object uqTaskPickerTasksPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME' FieldName = 'PROJECT_NAME'
......
...@@ -19,6 +19,7 @@ type ...@@ -19,6 +19,7 @@ type
uqSaveTaskRow: TUniQuery; uqSaveTaskRow: TUniQuery;
uqTaskItems: TUniQuery; uqTaskItems: TUniQuery;
uqAddTaskRow: TUniQuery; uqAddTaskRow: TUniQuery;
uqCopyTaskItem: TUniQuery;
uqTaskItemsTASK_ID: TStringField; uqTaskItemsTASK_ID: TStringField;
uqTaskItemsITEM_NUM: TShortintField; uqTaskItemsITEM_NUM: TShortintField;
uqTaskItemsAPPLICATION: TStringField; uqTaskItemsAPPLICATION: TStringField;
...@@ -93,12 +94,11 @@ type ...@@ -93,12 +94,11 @@ type
uqTaskPickerCustomers: TUniQuery; uqTaskPickerCustomers: TUniQuery;
uqTaskPickerProjects: TUniQuery; uqTaskPickerProjects: TUniQuery;
uqTaskPickerTasks: TUniQuery; uqTaskPickerTasks: TUniQuery;
uqTaskPickerCustomersCUSTOMER_ID: TStringField;
uqTaskPickerCustomersCUSTOMER_SHORT_NAME: TStringField;
uqTaskPickerProjectsPROJECT_ID: TStringField; uqTaskPickerProjectsPROJECT_ID: TStringField;
uqTaskPickerProjectsPROJECT_NAME: TStringField; uqTaskPickerProjectsPROJECT_NAME: TStringField;
uqTaskPickerProjectsCUSTOMER_ID: TStringField;
uqTaskPickerTasksTASK_ID: TStringField; uqTaskPickerTasksTASK_ID: TStringField;
uqTaskPickerTasksCUSTOMER_SHORT_NAME: TStringField; uqTaskPickerTasksCUSTOMER_NAME: TStringField;
uqTaskPickerTasksPROJECT_NAME: TStringField; uqTaskPickerTasksPROJECT_NAME: TStringField;
uqTaskPickerTasksTASK_NUM_1: TIntegerField; uqTaskPickerTasksTASK_NUM_1: TIntegerField;
uqTaskPickerTasksTASK_NUM_2: TIntegerField; uqTaskPickerTasksTASK_NUM_2: TIntegerField;
...@@ -107,6 +107,8 @@ type ...@@ -107,6 +107,8 @@ type
uqTaskPickerTasksTASK_NUM_5: TIntegerField; uqTaskPickerTasksTASK_NUM_5: TIntegerField;
uqTaskPickerTasksTASK_NUM_6: TIntegerField; uqTaskPickerTasksTASK_NUM_6: TIntegerField;
uqTaskPickerTasksTASK_SUBJECT: TStringField; uqTaskPickerTasksTASK_SUBJECT: TStringField;
uqTaskPickerCustomersCUSTOMER_ID: TStringField;
uqTaskPickerCustomersCUSTOMER_NAME: TStringField;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -120,6 +120,7 @@ type ...@@ -120,6 +120,7 @@ type
[HttpGet] function GetTaskItems(taskId: string): TTaskItemsResponse; [HttpGet] function GetTaskItems(taskId: string): TTaskItemsResponse;
[HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean; [HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
[HttpPost] function CopyTaskRow(taskId: string; sourceTaskItemId: Integer): Integer;
[HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; [HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
[HttpPost] function RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse; [HttpPost] function RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
......
...@@ -19,6 +19,7 @@ type ...@@ -19,6 +19,7 @@ type
function FindAssignedOptionId(const taskId, name: string): string; function FindAssignedOptionId(const taskId, name: string): string;
function FindAssignedOptionName(const taskId, name: string): string; function FindAssignedOptionName(const taskId, name: string): string;
function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean; function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
function CopyTaskRow(taskId: string; sourceTaskItemId: Integer): Integer;
function BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse; function BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
function RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse; function RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
...@@ -50,6 +51,7 @@ implementation ...@@ -50,6 +51,7 @@ implementation
procedure TTaskItemService.AfterConstruction; procedure TTaskItemService.AfterConstruction;
begin begin
inherited; inherited;
Logger.Log(4, 'TaskItemService.AfterConstruction->TApiDatabase.Create(nil)');
apiDB := TApiDatabase.Create(nil); apiDB := TApiDatabase.Create(nil);
Logger.Log(4, 'TaskItemService.AfterConstruction - apiDB created'); Logger.Log(4, 'TaskItemService.AfterConstruction - apiDB created');
end; end;
...@@ -272,6 +274,66 @@ begin ...@@ -272,6 +274,66 @@ begin
end; end;
function TTaskItemService.CopyTaskRow(taskId: string; sourceTaskItemId: Integer): Integer;
var
q: TUniQuery;
sourceItemNum: Integer;
begin
Logger.Log(4, Format('TaskItemService.CopyTaskRow - TASK_ID="%s" SOURCE_TASK_ITEM_ID="%d"', [taskId, sourceTaskItemId]));
apiDB.uqGetTaskRowPosition.Close;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ITEM_ID').AsInteger := sourceTaskItemId;
apiDB.uqGetTaskRowPosition.Open;
if apiDB.uqGetTaskRowPosition.IsEmpty then
begin
Logger.Log(2, Format('TaskItemService.CopyTaskRow - row not found TASK_ID="%s" TASK_ITEM_ID="%d"', [taskId, sourceTaskItemId]));
raise Exception.Create('Task row not found.');
end;
sourceItemNum := apiDB.uqGetTaskRowPositionITEM_NUM.AsInteger;
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
apiDB.ucETaskApi.StartTransaction;
try
apiDB.uqShiftTaskRowsForInsert.Close;
apiDB.uqShiftTaskRowsForInsert.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqShiftTaskRowsForInsert.ParamByName('INSERT_AFTER_ITEM_NUM').AsInteger := sourceItemNum;
apiDB.uqShiftTaskRowsForInsert.ExecSQL;
apiDB.uqCopyTaskItem.Close;
apiDB.uqCopyTaskItem.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqCopyTaskItem.ParamByName('TASK_ITEM_ID').AsInteger := sourceTaskItemId;
apiDB.uqCopyTaskItem.ParamByName('ITEM_NUM').AsInteger := sourceItemNum + 1;
apiDB.uqCopyTaskItem.ExecSQL;
if apiDB.uqCopyTaskItem.RowsAffected <> 1 then
raise Exception.Create('Unable to copy the selected task row.');
q.SQL.Text := 'select LAST_INSERT_ID() as TASK_ITEM_ID';
q.Open;
Result := q.FieldByName('TASK_ITEM_ID').AsInteger;
apiDB.ucETaskApi.Commit;
Logger.Log(4, Format('TaskItemService.CopyTaskRow - OK TASK_ID="%s" SOURCE_TASK_ITEM_ID="%d" TASK_ITEM_ID="%d" ITEM_NUM="%d"',
[taskId, sourceTaskItemId, Result, sourceItemNum + 1]));
except
on E: Exception do
begin
if apiDB.ucETaskApi.InTransaction then
apiDB.ucETaskApi.Rollback;
Logger.Log(2, 'TaskItemService.CopyTaskRow - ERROR: ' + E.Message);
raise;
end;
end;
finally
q.Free;
end;
end;
procedure TTaskItemService.MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer); procedure TTaskItemService.MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
var var
oldItemNum: Integer; oldItemNum: Integer;
......
...@@ -73,11 +73,11 @@ type ...@@ -73,11 +73,11 @@ type
value: string; value: string;
end; end;
type
TTaskPickerOption = class TTaskPickerOption = class
public public
value: string; value: string;
display: string; display: string;
customerId: string;
end; end;
TTaskPickerOptionsResponse = class TTaskPickerOptionsResponse = class
...@@ -87,6 +87,13 @@ type ...@@ -87,6 +87,13 @@ type
destructor Destroy; override; destructor Destroy; override;
end; end;
TTaskPickerTaskContext = class
public
taskId: string;
projectId: string;
customerId: 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}']
...@@ -95,9 +102,12 @@ type ...@@ -95,9 +102,12 @@ type
[HttpPost] function SaveTimeEntry(Item: TTimeEntrySave): string; [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; [HttpPost] function AddTaskToFocus(userId, taskId: string): Boolean;
function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse; [HttpPost] function RemoveTaskFromFocus(userId, taskId: string): Boolean;
function GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse; function GetTaskPickerCustomers: TTaskPickerOptionsResponse;
function GetTaskPickerProjects(customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(projectId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTaskContext(taskId: string): TTaskPickerTaskContext;
end; end;
implementation implementation
......
...@@ -34,10 +34,13 @@ type ...@@ -34,10 +34,13 @@ type
function AddTimeEntry(userId, taskDate: string): string; function AddTimeEntry(userId, taskDate: string): string;
function SaveTimeEntry(Item: TTimeEntrySave): string; function SaveTimeEntry(Item: TTimeEntrySave): string;
function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean; function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse; function GetTaskPickerCustomers: TTaskPickerOptionsResponse;
function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse; function GetTaskPickerProjects(customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse; function GetTaskPickerTasks(projectId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTaskContext(taskId: string): TTaskPickerTaskContext;
function DeleteTimeEntry(userId: string; entryId: Integer): Boolean; function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
function AddTaskToFocus(userId, taskId: string): Boolean;
function RemoveTaskFromFocus(userId, taskId: string): Boolean;
end; end;
implementation implementation
...@@ -50,7 +53,9 @@ uses ...@@ -50,7 +53,9 @@ uses
procedure TTimeEntryService.AfterConstruction; procedure TTimeEntryService.AfterConstruction;
begin begin
inherited; inherited;
Logger.Log(4, 'TTimeEntryService.AfterConstruction->TApiDatabase.Create(nil)');
apiDB := TApiDatabase.Create(nil); apiDB := TApiDatabase.Create(nil);
Logger.Log(4, 'TTimeEntryService.AfterConstruction - apiDB created')
end; end;
...@@ -130,7 +135,7 @@ var ...@@ -130,7 +135,7 @@ var
leftText: string; leftText: string;
taskText: string; taskText: string;
begin begin
customerText := FieldText(AQuery, 'CUSTOMER_SHORT_NAME'); customerText := FieldText(AQuery, 'CUSTOMER_NAME');
projectText := FieldText(AQuery, 'PROJECT_NAME'); projectText := FieldText(AQuery, 'PROJECT_NAME');
taskNumberText := BuildTaskNumber(AQuery); taskNumberText := BuildTaskNumber(AQuery);
subjectText := FieldText(AQuery, 'TASK_SUBJECT'); subjectText := FieldText(AQuery, 'TASK_SUBJECT');
...@@ -210,7 +215,7 @@ begin ...@@ -210,7 +215,7 @@ begin
' ti.CATEGORY, ' + ' ti.CATEGORY, ' +
' cat.CODE_DESC as CATEGORY_DESC, ' + ' cat.CODE_DESC as CATEGORY_DESC, ' +
' ti.SUMMARY, ' + ' ti.SUMMARY, ' +
' c.SHORT_NAME as CUSTOMER_SHORT_NAME, ' + ' c.SHORT_NAME as CUSTOMER_NAME, ' +
' p.NAME as PROJECT_NAME, ' + ' p.NAME as PROJECT_NAME, ' +
' t.TASK_NUM_1, ' + ' t.TASK_NUM_1, ' +
' t.TASK_NUM_2, ' + ' t.TASK_NUM_2, ' +
...@@ -290,7 +295,7 @@ begin ...@@ -290,7 +295,7 @@ begin
tasksQuery.SQL.Text := tasksQuery.SQL.Text :=
'select distinct ' + 'select distinct ' +
' t.TASK_ID, ' + ' t.TASK_ID, ' +
' c.SHORT_NAME as CUSTOMER_SHORT_NAME, ' + ' c.SHORT_NAME as CUSTOMER_NAME, ' +
' p.NAME as PROJECT_NAME, ' + ' p.NAME as PROJECT_NAME, ' +
' t.TASK_NUM_1, ' + ' t.TASK_NUM_1, ' +
' t.TASK_NUM_2, ' + ' t.TASK_NUM_2, ' +
...@@ -764,15 +769,14 @@ begin ...@@ -764,15 +769,14 @@ begin
end; end;
function TTimeEntryService.GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse; function TTimeEntryService.GetTaskPickerCustomers: TTaskPickerOptionsResponse;
begin begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerCustomers - USER_ID="%s"', [userId])); Logger.Log(4, 'TimeEntryService.GetTaskPickerCustomers Start');
Result := TTaskPickerOptionsResponse.Create; Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerCustomers.Close; apiDB.uqTaskPickerCustomers.Close;
apiDB.uqTaskPickerCustomers.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerCustomers.Open; apiDB.uqTaskPickerCustomers.Open;
try try
while not apiDB.uqTaskPickerCustomers.Eof do while not apiDB.uqTaskPickerCustomers.Eof do
...@@ -780,7 +784,7 @@ begin ...@@ -780,7 +784,7 @@ begin
AddTaskPickerOption( AddTaskPickerOption(
Result, Result,
FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_ID'), FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_ID'),
FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_SHORT_NAME') FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_NAME')
); );
apiDB.uqTaskPickerCustomers.Next; apiDB.uqTaskPickerCustomers.Next;
...@@ -791,15 +795,14 @@ begin ...@@ -791,15 +795,14 @@ begin
end; end;
function TTimeEntryService.GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse; function TTimeEntryService.GetTaskPickerProjects(customerId: string): TTaskPickerOptionsResponse;
begin begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerProjects - USER_ID="%s" CUSTOMER_ID="%s"', [userId, customerId])); Logger.Log(4, Format('TimeEntryService.GetTaskPickerProjects - CUSTOMER_ID="%s"', [customerId]));
Result := TTaskPickerOptionsResponse.Create; Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerProjects.Close; apiDB.uqTaskPickerProjects.Close;
apiDB.uqTaskPickerProjects.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerProjects.ParamByName('CUSTOMER_ID').AsString := customerId; apiDB.uqTaskPickerProjects.ParamByName('CUSTOMER_ID').AsString := customerId;
apiDB.uqTaskPickerProjects.Open; apiDB.uqTaskPickerProjects.Open;
try try
...@@ -811,6 +814,9 @@ begin ...@@ -811,6 +814,9 @@ begin
FieldText(apiDB.uqTaskPickerProjects, 'PROJECT_NAME') FieldText(apiDB.uqTaskPickerProjects, 'PROJECT_NAME')
); );
Result.options[Result.options.Count - 1].customerId :=
apiDB.uqTaskPickerProjectsCUSTOMER_ID.AsString;
apiDB.uqTaskPickerProjects.Next; apiDB.uqTaskPickerProjects.Next;
end; end;
finally finally
...@@ -819,15 +825,14 @@ begin ...@@ -819,15 +825,14 @@ begin
end; end;
function TTimeEntryService.GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse; function TTimeEntryService.GetTaskPickerTasks(projectId: string): TTaskPickerOptionsResponse;
begin begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerTasks - USER_ID="%s" PROJECT_ID="%s"', [userId, projectId])); Logger.Log(4, Format('TimeEntryService.GetTaskPickerTasks - PROJECT_ID="%s"', [projectId]));
Result := TTaskPickerOptionsResponse.Create; Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerTasks.Close; apiDB.uqTaskPickerTasks.Close;
apiDB.uqTaskPickerTasks.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerTasks.ParamByName('PROJECT_ID').AsString := projectId; apiDB.uqTaskPickerTasks.ParamByName('PROJECT_ID').AsString := projectId;
apiDB.uqTaskPickerTasks.Open; apiDB.uqTaskPickerTasks.Open;
try try
...@@ -847,6 +852,39 @@ begin ...@@ -847,6 +852,39 @@ begin
end; end;
function TTimeEntryService.GetTaskPickerTaskContext(taskId: string): TTaskPickerTaskContext;
var
q: TUniQuery;
begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerTaskContext - TASK_ID="%s"', [taskId]));
Result := TTaskPickerTaskContext.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
Result.taskId := taskId;
// Read-only lookup; an unknown task returns blank ids so the picker opens empty
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
q.SQL.Text :=
'select t.PROJECT_ID, p.CUSTOMER_ID ' +
'from tasks t ' +
'join project p on p.PROJECT_ID = t.PROJECT_ID ' +
'where t.TASK_ID = :TASK_ID';
q.ParamByName('TASK_ID').AsString := taskId;
q.Open;
if not q.Eof then
begin
Result.projectId := FieldText(q, 'PROJECT_ID');
Result.customerId := FieldText(q, 'CUSTOMER_ID');
end;
finally
q.Free;
end;
end;
function TTimeEntryService.DeleteTimeEntry(userId: string; entryId: Integer): Boolean; function TTimeEntryService.DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
var var
q: TUniQuery; q: TUniQuery;
...@@ -890,6 +928,105 @@ begin ...@@ -890,6 +928,105 @@ begin
end; end;
end; end;
function TTimeEntryService.AddTaskToFocus(userId, taskId: string): Boolean;
var
q: TUniQuery;
begin
Logger.Log(4, Format(
'TimeEntryService.AddTaskToFocus - USER_ID="%s" TASK_ID="%s"',
[userId, taskId]
));
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
q.Connection.StartTransaction;
try
q.SQL.Text :=
'select TASK_ID from task_assigned_user ' +
'where USER_ID = :USER_ID and TASK_ID = :TASK_ID';
q.ParamByName('USER_ID').AsString := userId;
q.ParamByName('TASK_ID').AsString := taskId;
q.Open;
if q.IsEmpty then
begin
q.Close;
q.SQL.Text :=
'insert into task_assigned_user ' +
'(USER_ID, TASK_ID, FOCUSED_TASK) ' +
'values (:USER_ID, :TASK_ID, ''T'')';
end
else
begin
q.Close;
q.SQL.Text :=
'update task_assigned_user ' +
'set FOCUSED_TASK = ''T'' ' +
'where USER_ID = :USER_ID and TASK_ID = :TASK_ID';
end;
q.ParamByName('USER_ID').AsString := userId;
q.ParamByName('TASK_ID').AsString := taskId;
q.ExecSQL;
q.Connection.Commit;
Result := True;
except
on E: Exception do
begin
if q.Connection.InTransaction then
q.Connection.Rollback;
Logger.Log(2, 'TimeEntryService.AddTaskToFocus - ERROR: ' + E.Message);
raise;
end;
end;
finally
q.Free;
end;
end;
function TTimeEntryService.RemoveTaskFromFocus(userId, taskId: string): Boolean;
var
q: TUniQuery;
begin
Logger.Log(4, Format(
'TimeEntryService.RemoveTaskFromFocus - USER_ID="%s" TASK_ID="%s"',
[userId, taskId]
));
// Only clears the flag; the task_assigned_user row is kept and never inserted here
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
q.Connection.StartTransaction;
try
q.SQL.Text :=
'update task_assigned_user ' +
'set FOCUSED_TASK = ''F'' ' +
'where USER_ID = :USER_ID and TASK_ID = :TASK_ID';
q.ParamByName('USER_ID').AsString := userId;
q.ParamByName('TASK_ID').AsString := taskId;
q.ExecSQL;
q.Connection.Commit;
Result := True;
except
on E: Exception do
begin
if q.Connection.InTransaction then
q.Connection.Rollback;
Logger.Log(2, 'TimeEntryService.RemoveTaskFromFocus - ERROR: ' + E.Message);
raise;
end;
end;
finally
q.Free;
end;
end;
initialization initialization
RegisterServiceType(TTimeEntryService); RegisterServiceType(TTimeEntryService);
......
[Settings] [Settings]
MemoLogLevel=4 MemoLogLevel=5
FileLogLevel=4 FileLogLevel=5
webClientVersion=0.9.3 webClientVersion=0.9.4.0
LogFileNum=245 LogFileNum=218
[Database] [Database]
Server=192.168.102.133 --Server=192.168.102.131
--Server=192.168.116.131 --Server=192.168.116.128
--Server=192.168.102.131
--Server=192.168.159.10 --Server=192.168.159.10
Server=192.168.90.12
Database=eTask Database=eTask
Username=root Username=root
Password=emsys01 Password=emsys01
......
...@@ -114,12 +114,11 @@ ...@@ -114,12 +114,11 @@
<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.9.3.1;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.4.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>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_AutoIncVersion>true</VerInfo_AutoIncVersion> <VerInfo_AutoIncVersion>true</VerInfo_AutoIncVersion>
<VerInfo_Build>1</VerInfo_Build> <VerInfo_Release>4</VerInfo_Release>
<VerInfo_Release>3</VerInfo_Release>
</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