Commit 33f30507 by Daniel Barker

Save and Cancel buttons now persist when editing a rows date

parent e3026c77
......@@ -85,9 +85,13 @@
Cancel
</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_task_picker_save" disabled>
<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>
</div>
</div>
</div>
......
......@@ -55,6 +55,8 @@ type
FTaskPickerOffCanvas: TTaskPickerOffCanvas;
FAddingNewEntry: Boolean;
FNewEntryRowIndex: Integer;
FEditingEntryId: Integer;
FOriginalTaskDate: string;
[async] procedure LoadTimeEntries;
function BeginAddEntry: Boolean;
......@@ -63,6 +65,8 @@ type
procedure BindNewEntryButtons;
procedure NewEntrySaveClick(Event: TJSEvent);
procedure NewEntryCancelClick(Event: TJSEvent);
[async] procedure EditedDateSaveClick(Event: TJSEvent);
procedure EditedDateCancelClick(Event: TJSEvent);
[async] procedure SaveNewEntry;
procedure CancelNewEntry;
procedure RenderTable;
......@@ -94,10 +98,11 @@ type
function GetTargetRowIndex(ATarget: TJSHTMLElement): Integer;
procedure DocumentMouseDown(Event: TJSEvent);
[async] procedure TaskPickerButtonClick(Event: TJSEvent);
procedure TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string; AAddedToFocus: Boolean);
procedure TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string);
procedure TaskPickerFocusChanged(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
procedure ApplyActiveRowState;
procedure EditorBlur(Event: TJSEvent);
[async] procedure SaveField(AIndex: Integer; const AFieldName: string);
[async] function SaveField(AIndex: Integer; const AFieldName: string): Boolean;
[async] procedure DeleteSelectedEntry;
function CopySelectedEntry: Boolean;
public
......@@ -135,7 +140,7 @@ begin
FAddingNewEntry := False;
FNewEntryRowIndex := -1;
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected, @TaskPickerFocusChanged);
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
......@@ -316,6 +321,7 @@ begin
el.innerText := displayName + '''s Time Entries';
end;
[async] procedure TFTimeEntries.LoadTimeEntries;
var
response: TXDataClientResponse;
......@@ -380,8 +386,6 @@ begin
end;
procedure TFTimeEntries.RenderTable;
var
host: TJSHTMLElement;
......@@ -565,7 +569,15 @@ begin
'</div>' +
'</td>' +
'</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;
Inc(rowIdx);
......@@ -716,6 +728,98 @@ begin
RenderTable;
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;
var
nodes: TJSNodeList;
......@@ -733,6 +837,22 @@ begin
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
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');
console.log('BindTableEditors: time-dd-item count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
......@@ -767,6 +887,7 @@ var
idxStr: string;
fieldName: string;
newVal: string;
actionsRow: TJSHTMLElement;
begin
if not xdwdsTimeEntries.Active then
Exit;
......@@ -788,6 +909,14 @@ begin
if xdwdsTimeEntries.Eof then
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']);
xdwdsTimeEntries.Edit;
......@@ -810,6 +939,15 @@ begin
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');
if IsNewEntryRow(idx) then
......@@ -1649,6 +1787,7 @@ end;
var
el: TJSHTMLElement;
idx: Integer;
taskId: string;
begin
Event.preventDefault;
Event.stopPropagation;
......@@ -1698,60 +1837,87 @@ begin
btnCopyEntry.Enabled := not IsNewEntryRow(idx);
if not Assigned(FTaskPickerOffCanvas) then
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected, @TaskPickerFocusChanged);
await(FTaskPickerOffCanvas.Open(FUserId, idx, FTaskOptions));
// 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, FTaskOptions, taskId));
end;
procedure TFTimeEntries.TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string; AAddedToFocus: Boolean);
procedure TFTimeEntries.TaskPickerFocusChanged(const ATaskId, ATaskDisplay: string; AFocused: Boolean);
var
btn: TJSHTMLElement;
triggerId: string;
i: Integer;
taskIndex: Integer;
taskOption: TJSObject;
taskFound: Boolean;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(ARowIndex);
if xdwdsTimeEntries.Eof then
Exit;
xdwdsTimeEntries.Edit;
xdwdsTimeEntriestaskId.AsString := ATaskId;
xdwdsTimeEntriestaskDisplay.AsString := ATaskDisplay;
xdwdsTimeEntries.Post;
FActiveRowIndex := ARowIndex;
btnDeleteEntry.Enabled := not IsNewEntryRow(ARowIndex);
btnCopyEntry.Enabled := not IsNewEntryRow(ARowIndex);
if AAddedToFocus then
begin
taskFound := False;
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
taskFound := True;
taskIndex := i;
Break;
end;
end;
if not taskFound then
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;
procedure TFTimeEntries.TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string);
var
btn: TJSHTMLElement;
triggerId: string;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(ARowIndex);
if xdwdsTimeEntries.Eof then
Exit;
xdwdsTimeEntries.Edit;
xdwdsTimeEntriestaskId.AsString := ATaskId;
xdwdsTimeEntriestaskDisplay.AsString := ATaskDisplay;
xdwdsTimeEntries.Post;
FActiveRowIndex := ARowIndex;
btnDeleteEntry.Enabled := not IsNewEntryRow(ARowIndex);
btnCopyEntry.Enabled := not IsNewEntryRow(ARowIndex);
CaptureTableScroll;
RenderTable;
......@@ -1833,17 +1999,27 @@ begin
Exit;
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);
SaveField(idx, fieldName);
end;
[async] procedure TFTimeEntries.SaveField(AIndex: Integer; const AFieldName: string);
[async] function TFTimeEntries.SaveField(AIndex: Integer; const AFieldName: string): Boolean;
var
response: TXDataClientResponse;
payload: TJSObject;
fieldValue: string;
begin
Result := False;
if not xdwdsTimeEntries.Active then
Exit;
......@@ -1891,7 +2067,7 @@ begin
'ITimeEntryService.SaveTimeEntryField',
[payload]
));
Result := True;
console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result)));
if SameText(AFieldName, 'hours') or SameText(AFieldName, 'taskDate') then
......
......@@ -49,4 +49,18 @@
.time-dd-toggle.is-invalid {
border-color: var(--bs-danger) !important;
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
......@@ -7,10 +7,14 @@ uses
type
TTaskPickedProc = reference to procedure(
ARowIndex: Integer;
const ATaskId, ATaskDisplay: string;
AAddedToFocus: Boolean
);
ARowIndex: Integer;
const ATaskId, ATaskDisplay: string
);
TTaskFocusChangedProc = reference to procedure(
const ATaskId, ATaskDisplay: string;
AFocused: Boolean
);
TTaskPickerOffCanvas = class
private
......@@ -18,10 +22,14 @@ type
FUserId: string;
FRowIndex: Integer;
FOnTaskPicked: TTaskPickedProc;
FOnFocusChanged: TTaskFocusChangedProc;
FCustomers: TJSArray;
FProjects: TJSArray;
FTasks: TJSArray;
FFocusedTasks: TJSArray;
FSavedFocus: Boolean;
FSaving: Boolean;
FPrefillToken: Integer;
procedure BindEvents;
procedure ClearSelect(const AElementId: string; ADisabled: Boolean);
......@@ -29,37 +37,53 @@ type
procedure FilterProjects(const ACustomerId: string);
function SelectedValue(const AElementId: 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 ClearStatus;
procedure SetSaveEnabled(AEnabled: Boolean);
procedure SetButtonEnabled(const AElementId: string; AEnabled: Boolean);
procedure UpdateButtons;
procedure Show;
procedure Hide;
[async] procedure LoadCustomers;
[async] procedure LoadProjects(const ACustomerId: string);
[async] procedure LoadTasks(const AProjectId: string);
[async] procedure PreselectTask(const ATaskId: string; AToken: Integer);
procedure SetSelectValue(const AElementId, AValue: string);
procedure CustomerChanged(Event: TJSEvent);
[async] procedure ProjectChanged(Event: TJSEvent);
procedure TaskChanged(Event: TJSEvent);
procedure FocusCheckboxChanged(Event: TJSEvent);
procedure UpdateFocusCheckbox;
procedure SelectClicked(Event: TJSEvent);
[async] procedure SaveClicked(Event: TJSEvent);
public
constructor Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc);
[async] procedure Open(const AUserId: string; ARowIndex: Integer; AFocusedTasks: TJSArray);
constructor Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc;
AOnFocusChanged: TTaskFocusChangedProc);
[async] procedure Open(const AUserId: string; ARowIndex: Integer; AFocusedTasks: TJSArray;
const ATaskId: string);
end;
implementation
constructor TTaskPickerOffCanvas.Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc);
constructor TTaskPickerOffCanvas.Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc;
AOnFocusChanged: TTaskFocusChangedProc);
begin
inherited Create;
FClient := AClient;
FOnTaskPicked := AOnTaskPicked;
FOnFocusChanged := AOnFocusChanged;
FCustomers := TJSArray.new;
FProjects := TJSArray.new;
FTasks := TJSArray.new;
FFocusedTasks := TJSArray.new;
FRowIndex := -1;
FSavedFocus := False;
FSaving := False;
FPrefillToken := 0;
BindEvents;
end;
......@@ -80,9 +104,17 @@ begin
if Assigned(el) then
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'));
if Assigned(el) then
el.addEventListener('click', TJSEventHandler(@SaveClicked));
el := TJSHTMLElement(document.getElementById('btn_task_picker_select'));
if Assigned(el) then
el.addEventListener('click', TJSEventHandler(@SelectClicked));
end;
procedure TTaskPickerOffCanvas.ClearSelect(const AElementId: string; ADisabled: Boolean);
......@@ -170,6 +202,58 @@ begin
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);
var
el: TJSHTMLElement;
......@@ -194,15 +278,38 @@ begin
el.classList.add('d-none');
end;
procedure TTaskPickerOffCanvas.SetSaveEnabled(AEnabled: Boolean);
procedure TTaskPickerOffCanvas.SetButtonEnabled(const AElementId: string; AEnabled: Boolean);
var
btn: TJSHTMLButtonElement;
begin
btn := TJSHTMLButtonElement(document.getElementById('btn_task_picker_save'));
btn := TJSHTMLButtonElement(document.getElementById(AElementId));
if Assigned(btn) then
btn.disabled := not AEnabled;
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;
begin
asm
......@@ -268,12 +375,77 @@ begin
FillSelect('task_picker_task', FTasks);
end;
procedure TTaskPickerOffCanvas.SetSelectValue(const AElementId, AValue: string);
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;
projectFound: Boolean;
i: Integer;
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;
SetSaveEnabled(False);
ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True);
UpdateFocusCheckbox;
......@@ -289,8 +461,8 @@ var
project: TJSObject;
i: Integer;
begin
Inc(FPrefillToken); // user took over; abandon any prefill still loading
ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_task', True);
UpdateFocusCheckbox;
......@@ -328,53 +500,66 @@ end;
procedure TTaskPickerOffCanvas.UpdateFocusCheckbox;
var
taskId: string;
taskOption: TJSObject;
alreadyFocused: Boolean;
addFocus: TJSHTMLInputElement;
i: Integer;
begin
taskId := SelectedValue('task_picker_task');
alreadyFocused := False;
if taskId <> '' then
begin
for i := 0 to FFocusedTasks.length - 1 do
begin
taskOption := TJSObject(FFocusedTasks[i]);
if string(taskOption['taskId']) = taskId then
begin
alreadyFocused := True;
Break;
end;
end;
end;
// Reset the checkbox to the task's saved focus state (discards any unsaved toggle)
FSavedFocus := IsTaskFocused(SelectedValue('task_picker_task'));
addFocus := TJSHTMLInputElement(
document.getElementById('task_picker_add_focus')
);
addFocus := FocusCheckbox;
if Assigned(addFocus) then
addFocus.checked := FSavedFocus;
addFocus.checked := alreadyFocused;
addFocus.disabled := alreadyFocused or (taskId = '');
UpdateButtons;
end;
procedure TTaskPickerOffCanvas.TaskChanged(Event: TJSEvent);
begin
Inc(FPrefillToken); // user took over; abandon any prefill still loading
ClearStatus;
UpdateFocusCheckbox;
SetSaveEnabled(SelectedValue('task_picker_task') <> '');
end;
procedure TTaskPickerOffCanvas.FocusCheckboxChanged(Event: TJSEvent);
begin
ClearStatus;
UpdateButtons;
end;
procedure TTaskPickerOffCanvas.SelectClicked(Event: TJSEvent);
var
taskId: string;
begin
Event.preventDefault;
taskId := SelectedValue('task_picker_task');
if taskId = '' then
begin
SetStatus('Select a task.');
Exit;
end;
ClearStatus;
// Sends the task to the grid only; focus changes happen through Save
if Assigned(FOnTaskPicked) then
FOnTaskPicked(FRowIndex, taskId, FindDisplay(FTasks, taskId));
Hide;
end;
[async] procedure TTaskPickerOffCanvas.SaveClicked(Event: TJSEvent);
var
taskId: string;
taskDisplay: string;
methodName: string;
addFocus: TJSHTMLInputElement;
addedToFocus: Boolean;
focused: Boolean;
begin
Event.preventDefault;
if FSaving then
Exit;
taskId := SelectedValue('task_picker_task');
if taskId = '' then
begin
......@@ -382,53 +567,79 @@ begin
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);
addFocus := TJSHTMLInputElement(
document.getElementById('task_picker_add_focus')
);
ClearStatus;
addedToFocus := False;
if focused then
methodName := 'ITimeEntryService.AddTaskToFocus'
else
methodName := 'ITimeEntryService.RemoveTaskFromFocus';
if addFocus.checked and not addFocus.disabled then
begin
SetSaveEnabled(False);
ClearStatus;
FSaving := True;
UpdateButtons;
try
try
await(FClient.RawInvokeAsync(
'ITimeEntryService.AddTaskToFocus',
[FUserId, taskId]
));
addedToFocus := True;
await(FClient.RawInvokeAsync(methodName, [FUserId, taskId]));
except
on E: EXDataClientRequestException do
begin
SetStatus(E.ErrorResult.ErrorMessage);
SetSaveEnabled(True);
Exit;
end;
on E: Exception do
begin
SetStatus('Unable to save focused task.');
Exit;
end;
end;
SetSaveEnabled(True);
end;
if Assigned(FOnTaskPicked) then
FOnTaskPicked(FRowIndex, taskId, taskDisplay, addedToFocus);
SetTaskFocused(taskId, taskDisplay, focused);
Hide;
// 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
AFocusedTasks: TJSArray;
const ATaskId: string
);
var
token: Integer;
begin
FUserId := AUserId;
FRowIndex := ARowIndex;
FFocusedTasks := AFocusedTasks;
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;
SetSaveEnabled(False);
ClearSelect('task_picker_customer', True);
ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True);
......@@ -442,7 +653,21 @@ begin
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
on E: Exception do
SetStatus('Unable to load this row''s task.');
end;
end;
......
......@@ -1342,7 +1342,7 @@ object ApiDatabase: TApiDatabase
SQL.Strings = (
'select distinct'
' t.TASK_ID,'
' c.NAME as CUSTOMER_NAME,'
' c.SHORT_NAME as CUSTOMER_NAME,'
' p.NAME as PROJECT_NAME,'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
......
......@@ -73,7 +73,6 @@ type
value: string;
end;
type
TTaskPickerOption = class
public
value: string;
......@@ -88,6 +87,13 @@ type
destructor Destroy; override;
end;
TTaskPickerTaskContext = class
public
taskId: string;
projectId: string;
customerId: string;
end;
[ServiceContract, Model(API_MODEL)]
ITimeEntryService = interface(IInvokable)
['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}']
......@@ -97,9 +103,11 @@ type
[HttpPost] function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
[HttpPost] function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
[HttpPost] function AddTaskToFocus(userId, taskId: string): Boolean;
[HttpPost] function RemoveTaskFromFocus(userId, taskId: string): Boolean;
function GetTaskPickerCustomers: TTaskPickerOptionsResponse;
function GetTaskPickerProjects(customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(projectId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTaskContext(taskId: string): TTaskPickerTaskContext;
end;
implementation
......
......@@ -37,8 +37,10 @@ type
function GetTaskPickerCustomers: TTaskPickerOptionsResponse;
function GetTaskPickerProjects(customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(projectId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTaskContext(taskId: string): TTaskPickerTaskContext;
function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
function AddTaskToFocus(userId, taskId: string): Boolean;
function RemoveTaskFromFocus(userId, taskId: string): Boolean;
end;
implementation
......@@ -213,7 +215,7 @@ begin
' ti.CATEGORY, ' +
' cat.CODE_DESC as CATEGORY_DESC, ' +
' ti.SUMMARY, ' +
' c.NAME as CUSTOMER_NAME, ' +
' c.SHORT_NAME as CUSTOMER_NAME, ' +
' p.NAME as PROJECT_NAME, ' +
' t.TASK_NUM_1, ' +
' t.TASK_NUM_2, ' +
......@@ -293,7 +295,7 @@ begin
tasksQuery.SQL.Text :=
'select distinct ' +
' t.TASK_ID, ' +
' c.NAME as CUSTOMER_NAME, ' +
' c.SHORT_NAME as CUSTOMER_NAME, ' +
' p.NAME as PROJECT_NAME, ' +
' t.TASK_NUM_1, ' +
' t.TASK_NUM_2, ' +
......@@ -308,7 +310,7 @@ begin
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID ' +
'where tau.USER_ID = :USER_ID ' +
' and upper(trim(coalesce(tau.FOCUSED_TASK, ''''))) = ''T'' ' +
'order by c.NAME, p.NAME, t.TASK_NUM_1, t.TASK_NUM_2, t.TASK_NUM_3, t.TASK_NUM_4, t.TASK_NUM_5, t.TASK_NUM_6';
'order by c.SHORT_NAME, p.NAME, t.TASK_NUM_1, t.TASK_NUM_2, t.TASK_NUM_3, t.TASK_NUM_4, t.TASK_NUM_5, t.TASK_NUM_6';
tasksQuery.ParamByName('USER_ID').AsString := userId;
tasksQuery.Open;
......@@ -850,6 +852,39 @@ begin
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;
var
q: TUniQuery;
......@@ -952,6 +987,47 @@ begin
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
RegisterServiceType(TTimeEntryService);
......
......@@ -2,7 +2,7 @@
MemoLogLevel=5
FileLogLevel=5
webClientVersion=0.9.3
LogFileNum=208
LogFileNum=218
[Database]
--Server=192.168.102.131
......
......@@ -114,11 +114,11 @@
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_ExeOutput>.\bin</DCC_ExeOutput>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.9.3.18;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.3.25;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys>
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_AutoIncVersion>true</VerInfo_AutoIncVersion>
<VerInfo_Build>18</VerInfo_Build>
<VerInfo_Build>25</VerInfo_Build>
<VerInfo_Release>3</VerInfo_Release>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
......
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