Commit 7a8e3164 by Mac Stephens

Add field-level time entry saving, focused task dropdowns, place support, and delete entry flow

parent 29263ec6
...@@ -160,6 +160,12 @@ begin ...@@ -160,6 +160,12 @@ begin
var btnRight = document.getElementById('btn_confirm_right'); var btnRight = document.getElementById('btn_confirm_right');
var bsModal; var bsModal;
console.log('confirmation modal=', modal);
console.log('confirmation body=', body);
console.log('confirmation left=', btnLeft);
console.log('confirmation right=', btnRight);
if (body) body.innerText = msg; if (body) body.innerText = msg;
if (btnLeft) btnLeft.innerText = leftLabel; if (btnLeft) btnLeft.innerText = leftLabel;
if (btnRight) btnRight.innerText = rightLabel; if (btnRight) btnRight.innerText = rightLabel;
......
...@@ -87,19 +87,23 @@ ...@@ -87,19 +87,23 @@
</div> </div>
<!-- Confirmation Modal --> <!-- Confirmation Modal -->
<div class="modal fade" id="mdl_confirmation" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content shadow-lg"> <div class="modal-content shadow-lg">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Confirm</h5> <h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body fw-bold" id="lbl_confirmation_body"> <div class="modal-body fw-bold" id="main_modal_body">
Placeholder text Placeholder text
</div> </div>
<div class="modal-footer justify-content-center"> <div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary me-3" id="btn_confirm_left">Cancel</button> <button type="button" class="btn btn-danger me-3" id="btn_confirm_left">
<button type="button" class="btn btn-secondary" id="btn_confirm_right">Confirm</button> Delete
</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">
Cancel
</button>
</div> </div>
</div> </div>
</div> </div>
......
object FTimeEntries: TFTimeEntries object FTimeEntries: TFTimeEntries
Width = 640 Width = 640
Height = 480 Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS ElementFont = efCSS
OnCreate = WebFormCreate OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel object lblValidationMessage: TWebLabel
Left = 90 Left = 90
Top = 32 Top = 32
Width = 69 Width = 3
Height = 15 Height = 15
ElementID = 'lbl_validation_message' ElementID = 'lbl_validation_message'
ElementFont = efCSS ElementFont = efCSS
...@@ -26,7 +27,7 @@ object FTimeEntries: TFTimeEntries ...@@ -26,7 +27,7 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False ShowFocus = False
Text = 'edtWeekOf' Text = 'edtWeekOf'
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnChange = edtWeekOfChange OnExit = edtWeekOfExit
end end
object edtStartDate: TWebEdit object edtStartDate: TWebEdit
Left = 245 Left = 245
...@@ -41,7 +42,6 @@ object FTimeEntries: TFTimeEntries ...@@ -41,7 +42,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False ShowFocus = False
Text = 'edtStartDate' Text = 'edtStartDate'
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnChange = edtStartDateChange
end end
object edtEndDate: TWebEdit object edtEndDate: TWebEdit
Left = 415 Left = 415
...@@ -56,7 +56,6 @@ object FTimeEntries: TFTimeEntries ...@@ -56,7 +56,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False ShowFocus = False
Text = 'edtEndDate' Text = 'edtEndDate'
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnChange = edtEndDateChange
end end
object btnAddEntry: TWebButton object btnAddEntry: TWebButton
Left = 440 Left = 440
...@@ -72,6 +71,33 @@ object FTimeEntries: TFTimeEntries ...@@ -72,6 +71,33 @@ object FTimeEntries: TFTimeEntries
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnAddEntryClick OnClick = btnAddEntryClick
end end
object btnDeleteEntry: TWebButton
Left = 338
Top = 24
Width = 96
Height = 25
Caption = 'Delete Entry'
ChildOrder = 5
ElementID = 'btn_delete_entry'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnDeleteEntryClick
end
object btnSearchRange: TWebButton
Left = 236
Top = 24
Width = 96
Height = 25
Caption = 'Search Range'
ChildOrder = 6
ElementID = 'btn_search_range'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnSearchRangeClick
end
object xdwcTimeEntries: TXDataWebClient object xdwcTimeEntries: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 460 Left = 460
...@@ -107,5 +133,11 @@ object FTimeEntries: TFTimeEntries ...@@ -107,5 +133,11 @@ object FTimeEntries: TFTimeEntries
object xdwdsTimeEntriessummary: TStringField object xdwdsTimeEntriessummary: TStringField
FieldName = 'summary' FieldName = 'summary'
end end
object xdwdsTimeEntriesplace: TStringField
FieldName = 'place'
end
object xdwdsTimeEntriesplaceDesc: TStringField
FieldName = 'placeDesc'
end
end end
end end
...@@ -7,22 +7,74 @@ ...@@ -7,22 +7,74 @@
<div class="d-flex align-items-center gap-2 flex-nowrap"> <div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_week_of" class="form-label mb-0 text-nowrap small">Week of</label> <label for="edt_week_of" class="form-label mb-0 text-nowrap small">Week of</label>
<input id="edt_week_of" type="date" class="form-control form-control-sm time-date-picker"> <input id="edt_week_of" type="date" class="form-control form-control-sm time-date-picker" />
</div> </div>
<div class="d-flex align-items-center gap-2 flex-nowrap"> <div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_start_date" class="form-label mb-0 text-nowrap small">Start</label> <label for="edt_start_date" class="form-label mb-0 text-nowrap small">Start</label>
<input id="edt_start_date" type="date" class="form-control form-control-sm time-date-picker"> <input id="edt_start_date" type="date" class="form-control form-control-sm time-date-picker" />
</div> </div>
<div class="d-flex align-items-center gap-2 flex-nowrap"> <div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_end_date" class="form-label mb-0 text-nowrap small">End</label> <label for="edt_end_date" class="form-label mb-0 text-nowrap small">End</label>
<input id="edt_end_date" type="date" class="form-control form-control-sm time-date-picker"> <input id="edt_end_date" type="date" class="form-control form-control-sm time-date-picker" />
</div> </div>
<button id="btn_add_entry" class="btn btn-sm btn-success text-nowrap">Add Entry</button> <button id="btn_search_range" class="btn btn-sm btn-primary text-nowrap">
Search Range
</button>
<button id="btn_add_entry" class="btn btn-sm btn-success text-nowrap">
Add Entry
</button>
<button id="btn_delete_entry" class="btn btn-sm btn-danger text-nowrap" disabled>
Delete Entry
</button>
</div> </div>
</div> </div>
<div id="lbl_validation_message" class="alert alert-danger py-1 px-2 mb-2 d-none small"></div> <div id="lbl_validation_message" class="alert alert-danger py-1 px-2 mb-2 d-none small"></div>
<div id="time_entries_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div> <div id="time_entries_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="task_picker_offcanvas" aria-labelledby="task_picker_title">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="task_picker_title">Find Task</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="mb-3">
<label for="task_picker_customer" class="form-label">Customer</label>
<select id="task_picker_customer" class="form-select form-select-sm">
<option value=""></option>
</select>
</div>
<div class="mb-3">
<label for="task_picker_project" class="form-label">Project</label>
<select id="task_picker_project" class="form-select form-select-sm" disabled>
<option value=""></option>
</select>
</div>
<div class="mb-3">
<label for="task_picker_task" class="form-label">Task</label>
<select id="task_picker_task" class="form-select form-select-sm" disabled>
<option value=""></option>
</select>
</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">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas">
Cancel
</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_task_picker_save" disabled>
Save
</button>
</div>
</div>
</div>
</div> </div>
...@@ -4,8 +4,8 @@ interface ...@@ -4,8 +4,8 @@ interface
uses uses
System.SysUtils, System.Classes, System.DateUtils, JS, Web, WEBLib.Graphics, WEBLib.Controls, System.SysUtils, System.Classes, System.DateUtils, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.ExtCtrls, WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.ExtCtrls, System.StrUtils,
XData.Web.Client, ConnectionModule, XData.Web.JsonDataset, Data.DB, XData.Web.Dataset; XData.Web.Client, ConnectionModule, XData.Web.JsonDataset, Data.DB, XData.Web.Dataset, uTaskPickerOffCanvas;
type type
TFTimeEntries = class(TWebForm) TFTimeEntries = class(TWebForm)
...@@ -25,11 +25,15 @@ type ...@@ -25,11 +25,15 @@ type
xdwdsTimeEntriescategoryDesc: TStringField; xdwdsTimeEntriescategoryDesc: TStringField;
btnAddEntry: TWebButton; btnAddEntry: TWebButton;
lblValidationMessage: TWebLabel; lblValidationMessage: TWebLabel;
procedure edtWeekOfChange(Sender: TObject); xdwdsTimeEntriesplace: TStringField;
procedure edtStartDateChange(Sender: TObject); xdwdsTimeEntriesplaceDesc: TStringField;
procedure edtEndDateChange(Sender: TObject); btnDeleteEntry: TWebButton;
btnSearchRange: TWebButton;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
[async] procedure btnAddEntryClick(Sender: TObject); [async] procedure btnAddEntryClick(Sender: TObject);
procedure btnDeleteEntryClick(Sender: TObject);
procedure edtWeekOfExit(Sender: TObject);
[async] procedure btnSearchRangeClick(Sender: TObject);
private private
FUserId: string; FUserId: string;
FUserName: string; FUserName: string;
...@@ -37,6 +41,7 @@ type ...@@ -37,6 +41,7 @@ type
FEndDate: string; FEndDate: string;
FTaskOptions: TJSArray; FTaskOptions: TJSArray;
FCategoryOptions: TJSArray; FCategoryOptions: TJSArray;
FPlaceOptions: TJSArray;
FUpdatingDates: Boolean; FUpdatingDates: Boolean;
FPendingScrollTop: Integer; FPendingScrollTop: Integer;
FPendingScrollLeft: Integer; FPendingScrollLeft: Integer;
...@@ -44,6 +49,7 @@ type ...@@ -44,6 +49,7 @@ type
FPendingEntryId: string; FPendingEntryId: string;
FLastMouseDownRowIndex: Integer; FLastMouseDownRowIndex: Integer;
FLastMouseDownTime: Double; FLastMouseDownTime: Double;
FTaskPickerOffCanvas: TTaskPickerOffCanvas;
[async] procedure LoadTimeEntries; [async] procedure LoadTimeEntries;
[async] function AddTimeEntry: Boolean; [async] function AddTimeEntry: Boolean;
procedure RenderTable; procedure RenderTable;
...@@ -69,10 +75,15 @@ type ...@@ -69,10 +75,15 @@ type
procedure ClearRowValidation(AIndex: Integer); procedure ClearRowValidation(AIndex: Integer);
procedure ShowRowValidationMessage(AIndex: Integer; const AMessage: string); procedure ShowRowValidationMessage(AIndex: Integer; const AMessage: string);
procedure HideRowValidationMessage; procedure HideRowValidationMessage;
[async] procedure SaveRow(AIndex: Integer);
procedure SetTopControlsEnabled(AEnabled: Boolean); procedure SetTopControlsEnabled(AEnabled: Boolean);
function GetTargetRowIndex(ATarget: TJSHTMLElement): Integer; function GetTargetRowIndex(ATarget: TJSHTMLElement): Integer;
procedure DocumentMouseDown(Event: TJSEvent); procedure DocumentMouseDown(Event: TJSEvent);
[async] procedure TaskPickerButtonClick(Event: TJSEvent);
procedure TaskPickerTaskSelected(ARowIndex: Integer; const ATaskId, ATaskDisplay: string);
procedure ApplyActiveRowState;
procedure EditorBlur(Event: TJSEvent);
[async] procedure SaveField(AIndex: Integer; const AFieldName: string);
[async] procedure DeleteSelectedEntry;
public public
end; end;
...@@ -96,6 +107,7 @@ begin ...@@ -96,6 +107,7 @@ begin
FTaskOptions := TJSArray.new; FTaskOptions := TJSArray.new;
FCategoryOptions := TJSArray.new; FCategoryOptions := TJSArray.new;
FPlaceOptions := TJSArray.new;
FUpdatingDates := False; FUpdatingDates := False;
FPendingScrollTop := 0; FPendingScrollTop := 0;
FPendingScrollLeft := 0; FPendingScrollLeft := 0;
...@@ -104,6 +116,8 @@ begin ...@@ -104,6 +116,8 @@ begin
FLastMouseDownRowIndex := -1; FLastMouseDownRowIndex := -1;
FLastMouseDownTime := 0; FLastMouseDownTime := 0;
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown)); document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
payload := AuthService.TokenPayload; payload := AuthService.TokenPayload;
...@@ -117,6 +131,7 @@ begin ...@@ -117,6 +131,7 @@ begin
end; end;
SetTimeEntriesLabel(FUserName); SetTimeEntriesLabel(FUserName);
btnDeleteEntry.Enabled := False;
anchorDate := Application.Parameters.Values['date']; anchorDate := Application.Parameters.Values['date'];
if anchorDate = '' then if anchorDate = '' then
...@@ -196,32 +211,19 @@ begin ...@@ -196,32 +211,19 @@ begin
LoadTimeEntries; LoadTimeEntries;
end; end;
procedure TFTimeEntries.edtWeekOfChange(Sender: TObject);
begin
if FUpdatingDates then
Exit;
SetWeekRangeFromAnchor(edtWeekOf.Text, True);
end;
procedure TFTimeEntries.edtStartDateChange(Sender: TObject); procedure TFTimeEntries.edtWeekOfExit(Sender: TObject);
begin begin
if FUpdatingDates then if FUpdatingDates then
Exit; Exit;
FStartDate := edtStartDate.Text; if edtWeekOf.Text = '' then
LoadTimeEntries;
end;
procedure TFTimeEntries.edtEndDateChange(Sender: TObject);
begin
if FUpdatingDates then
Exit; Exit;
FEndDate := edtEndDate.Text; SetWeekRangeFromAnchor(edtWeekOf.Text, True);
LoadTimeEntries;
end; end;
procedure TFTimeEntries.GotoRowIndex(AIndex: Integer); procedure TFTimeEntries.GotoRowIndex(AIndex: Integer);
var var
i: Integer; i: Integer;
...@@ -302,6 +304,7 @@ begin ...@@ -302,6 +304,7 @@ begin
FTaskOptions := TJSArray(resultObj['taskOptions']); FTaskOptions := TJSArray(resultObj['taskOptions']);
FCategoryOptions := TJSArray(resultObj['categoryOptions']); FCategoryOptions := TJSArray(resultObj['categoryOptions']);
FPlaceOptions := TJSArray(resultObj['placeOptions']);
itemsArray := TJSArray(resultObj['items']); itemsArray := TJSArray(resultObj['items']);
xdwdsTimeEntries.Close; xdwdsTimeEntries.Close;
...@@ -311,6 +314,7 @@ begin ...@@ -311,6 +314,7 @@ begin
FActiveRowIndex := -1; FActiveRowIndex := -1;
HideRowValidationMessage; HideRowValidationMessage;
SetTopControlsEnabled(True); SetTopControlsEnabled(True);
btnDeleteEntry.Enabled := False;
RenderTable; RenderTable;
finally finally
...@@ -322,6 +326,7 @@ end; ...@@ -322,6 +326,7 @@ end;
[async] function TFTimeEntries.AddTimeEntry: Boolean; [async] function TFTimeEntries.AddTimeEntry: Boolean;
var var
response: TXDataClientResponse; response: TXDataClientResponse;
resultObj: TJSObject;
begin begin
Result := False; Result := False;
...@@ -337,15 +342,25 @@ begin ...@@ -337,15 +342,25 @@ begin
Exit; Exit;
end; end;
if (FActiveRowIndex >= 0) and (not ValidateRow(FActiveRowIndex)) then
begin
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before adding another entry.');
SetTopControlsEnabled(False);
Exit;
end;
try try
response := await(xdwcTimeEntries.RawInvokeAsync( response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.AddTimeEntry', 'ITimeEntryService.AddTimeEntry',
[FUserId, edtWeekOf.Text] [FUserId, edtWeekOf.Text]
)); ));
FPendingEntryId := JS.toString(response.Result); resultObj := TJSObject(response.Result);
FPendingEntryId := string(resultObj['value']);
console.log('AddTimeEntry server entry id=' + FPendingEntryId);
console.log('AddTimeEntry response=' + string(TJSJSON.stringify(response.Result)));
Result := True; Result := True;
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
...@@ -407,7 +422,7 @@ var ...@@ -407,7 +422,7 @@ var
'value="' + HtmlEncode(Value) + '">'; 'value="' + HtmlEncode(Value) + '">';
end; end;
function SelectList(const FieldName, CurrentValue, CurrentDisplay: string; const AIdx: Integer; const Items: TJSArray; const ValueProp, DisplayProp: string): string; function SelectList(const FieldName, CurrentValue, CurrentDisplay: string; const AIdx: Integer; const Items: TJSArray; const ValueProp, DisplayProp: string; AAllowBlank, AShowTaskPicker: Boolean): string;
var var
i: Integer; i: Integer;
optionObj: TJSObject; optionObj: TJSObject;
...@@ -417,24 +432,33 @@ var ...@@ -417,24 +432,33 @@ var
begin begin
triggerId := 'time_dd_' + FieldName + '_' + IntToStr(AIdx); triggerId := 'time_dd_' + FieldName + '_' + IntToStr(AIdx);
Result := Result := '';
'<div class="dropdown w-100">' +
'<button id="' + triggerId + '" class="btn btn-sm border w-100 d-flex justify-content-between align-items-center text-start time-dd-toggle btn-light" ' + if AShowTaskPicker then
Result := Result +
'<div class="input-group input-group-sm flex-nowrap w-100">';
Result := Result +
'<div class="dropdown flex-grow-1" style="min-width:0;">' +
'<button id="' + triggerId + '" class="btn btn-sm border w-100 d-flex justify-content-between align-items-center text-start time-dd-toggle btn-light' +
IfThen(AShowTaskPicker, ' rounded-end-0', '') + '" ' +
'type="button" data-bs-toggle="dropdown" aria-expanded="false" ' + 'type="button" data-bs-toggle="dropdown" aria-expanded="false" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '">' + 'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '">' +
'<span class="time-dd-label text-truncate">' + HtmlEncode(CurrentDisplay) + '</span>' + '<span class="time-dd-label text-truncate">' + HtmlEncode(CurrentDisplay) + '</span>' +
'<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>' + '<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>' +
'</button>' + '</button>' +
'<div class="dropdown-menu w-100 p-0 overflow-hidden">'; '<div class="dropdown-menu w-100 p-0 pe-1 overflow-hidden">';
if AAllowBlank then
begin
Result := Result + Result := Result +
'<button type="button" class="dropdown-item time-dd-item" ' + '<button type="button" class="dropdown-item time-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' + 'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' + 'data-field="' + FieldName + '" ' +
'data-value="" ' + 'data-value="" ' +
'data-display="" ' + 'data-display="" ' +
'data-trigger-id="' + triggerId + '"></button>'; 'data-trigger-id="' + triggerId + '">&nbsp;</button>';
end;
for i := 0 to Items.length - 1 do for i := 0 to Items.length - 1 do
begin begin
...@@ -454,6 +478,14 @@ var ...@@ -454,6 +478,14 @@ var
Result := Result + Result := Result +
'</div>' + '</div>' +
'</div>'; '</div>';
if AShowTaskPicker then
begin
Result := Result +
'<button type="button" class="btn btn-outline-secondary time-task-picker-btn flex-shrink-0 text-nowrap" ' +
'data-idx="' + IntToStr(AIdx) + '" title="Find task">Find</button>' +
'</div>';
end;
end; end;
function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string; function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string;
...@@ -470,12 +502,13 @@ begin ...@@ -470,12 +502,13 @@ begin
html := html :=
'<div class="time-vscroll">' + '<div class="time-vscroll">' +
'<div class="time-hscroll">' + '<div class="time-hscroll">' +
'<table class="table table-sm table-bordered align-middle mb-0 time-table" style="min-width: 1550px; table-layout: fixed;">' + '<table class="table table-sm table-bordered align-middle mb-0 time-table" style="min-width: 1710px; table-layout: fixed;">' +
'<colgroup>' + '<colgroup>' +
'<col style="width:140px">' + // Date '<col style="width:140px">' + // Date
'<col style="width:500px">' + // Task '<col style="width:500px">' + // Task
'<col style="width:90px">' + // Hours '<col style="width:90px">' + // Hours
'<col style="width:150px">' + // Time '<col style="width:150px">' + // Time
'<col style="width:160px">' + // Place
'<col style="width:190px">' + // Category '<col style="width:190px">' + // Category
'<col style="width:480px">' + // Summary '<col style="width:480px">' + // Summary
'</colgroup>' + '</colgroup>' +
...@@ -484,6 +517,7 @@ begin ...@@ -484,6 +517,7 @@ begin
Th('Task') + Th('Task') +
Th('Hours') + Th('Hours') +
Th('Time') + Th('Time') +
Th('Place') +
Th('Category') + Th('Category') +
Th('Summary') + Th('Summary') +
'</tr></thead><tbody>'; '</tr></thead><tbody>';
...@@ -500,10 +534,11 @@ begin ...@@ -500,10 +534,11 @@ begin
html := html + html := html +
'<tr class="time-row-selectable" data-idx="' + IntToStr(rowIdx) + '" data-entry-id="' + IntToStr(xdwdsTimeEntriesentryId.AsInteger) + '" data-task-id="' + xdwdsTimeEntriestaskId.AsString + '">' + '<tr class="time-row-selectable" data-idx="' + IntToStr(rowIdx) + '" data-entry-id="' + IntToStr(xdwdsTimeEntriesentryId.AsInteger) + '" data-task-id="' + xdwdsTimeEntriestaskId.AsString + '">' +
TdNowrap(DateInput('taskDate', xdwdsTimeEntriestaskDate.AsString, rowIdx)) + TdNowrap(DateInput('taskDate', xdwdsTimeEntriestaskDate.AsString, rowIdx)) +
TdNowrap(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay')) + TdNowrap(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay', False, True)) +
TdNowrap(HoursInput('hours', hoursText, rowIdx)) + TdNowrap(HoursInput('hours', hoursText, rowIdx)) +
TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) + TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) +
TdNowrap(SelectList('category', xdwdsTimeEntriescategory.AsString, xdwdsTimeEntriescategoryDesc.AsString, rowIdx, FCategoryOptions, 'code', 'codeDesc')) + TdNowrap(SelectList('place', xdwdsTimeEntriesplace.AsString, xdwdsTimeEntriesplaceDesc.AsString, rowIdx, FPlaceOptions, 'code', 'codeDesc', True, False)) +
TdNowrap(SelectList('category', xdwdsTimeEntriescategory.AsString, xdwdsTimeEntriescategoryDesc.AsString, rowIdx, FCategoryOptions, 'code', 'codeDesc', False, False)) +
TdWrap(SummaryTextArea('summary', xdwdsTimeEntriessummary.AsString, rowIdx)) + TdWrap(SummaryTextArea('summary', xdwdsTimeEntriessummary.AsString, rowIdx)) +
'</tr>'; '</tr>';
...@@ -512,6 +547,7 @@ begin ...@@ -512,6 +547,7 @@ begin
end; end;
html := html + '</tbody></table></div></div>'; html := html + '</tbody></table></div></div>';
SetTotalRowsLabel(rowIdx);
host.innerHTML := html; host.innerHTML := html;
BindTableEditors; BindTableEditors;
...@@ -519,6 +555,7 @@ begin ...@@ -519,6 +555,7 @@ begin
EnableColumnResize; EnableColumnResize;
RestoreTableScroll; RestoreTableScroll;
ApplyPendingEntryFocus; ApplyPendingEntryFocus;
ApplyActiveRowState;
end; end;
procedure TFTimeEntries.BindTableEditors; procedure TFTimeEntries.BindTableEditors;
...@@ -535,6 +572,7 @@ begin ...@@ -535,6 +572,7 @@ begin
begin begin
el := TJSHTMLElement(nodes.item(i)); el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput)); el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
end; end;
nodes := document.querySelectorAll('.time-dd-item'); nodes := document.querySelectorAll('.time-dd-item');
...@@ -545,6 +583,14 @@ begin ...@@ -545,6 +583,14 @@ begin
el.addEventListener('click', TJSEventHandler(@DropdownItemClick)); el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
end; end;
nodes := document.querySelectorAll('.time-task-picker-btn');
console.log('BindTableEditors: time-task-picker-btn count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@TaskPickerButtonClick));
end;
nodes := document.querySelectorAll('.time-row-selectable'); nodes := document.querySelectorAll('.time-row-selectable');
console.log('BindTableEditors: time-row-selectable count=' + IntToStr(nodes.length)); console.log('BindTableEditors: time-row-selectable count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do for i := 0 to nodes.length - 1 do
...@@ -577,6 +623,7 @@ begin ...@@ -577,6 +623,7 @@ begin
Exit; Exit;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
GotoRowIndex(idx); GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then if xdwdsTimeEntries.Eof then
...@@ -616,65 +663,6 @@ begin ...@@ -616,65 +663,6 @@ begin
end; end;
[async] procedure TFTimeEntries.SaveRow(AIndex: Integer);
var
saveObj: TJSObject;
response: TXDataClientResponse;
rowEl: TJSHTMLElement;
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
if not ValidateRow(AIndex) then
begin
ApplyRowValidation(AIndex);
ShowRowValidationMessage(AIndex, 'Complete required fields before leaving this row.');
Exit;
end;
saveObj := TJSObject.new;
saveObj['entryId'] := xdwdsTimeEntriesentryId.AsString;
saveObj['userId'] := FUserId;
saveObj['taskDate'] := xdwdsTimeEntriestaskDate.AsString;
saveObj['taskId'] := xdwdsTimeEntriestaskId.AsString;
saveObj['hours'] := xdwdsTimeEntrieshours.AsFloat;
saveObj['taskTime'] := xdwdsTimeEntriestaskTime.AsString;
saveObj['category'] := xdwdsTimeEntriescategory.AsString;
saveObj['summary'] := xdwdsTimeEntriessummary.AsString;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.SaveTimeEntry',
[saveObj]
));
console.log('SaveRow response=' + string(TJSJSON.stringify(response.Result)));
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
nodes := rowEl.querySelectorAll('[data-unsaved-data="1"]');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.removeAttribute('data-unsaved-data');
end;
ClearRowValidation(AIndex);
except
on E: EXDataClientRequestException do
begin
console.log('SaveRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTimeEntries.RowClick(Event: TJSEvent); procedure TFTimeEntries.RowClick(Event: TJSEvent);
var var
rowEl: TJSHTMLElement; rowEl: TJSHTMLElement;
...@@ -686,7 +674,22 @@ begin ...@@ -686,7 +674,22 @@ begin
if idx < 0 then if idx < 0 then
Exit; Exit;
if (FActiveRowIndex >= 0) and
(idx <> FActiveRowIndex) and
(not ValidateRow(FActiveRowIndex)) then
begin
Event.preventDefault;
Event.stopPropagation;
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before leaving this row.');
ApplyActiveRowState;
Exit;
end;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
ApplyActiveRowState;
end; end;
...@@ -713,6 +716,7 @@ begin ...@@ -713,6 +716,7 @@ begin
if not ValidateRow(idx) then if not ValidateRow(idx) then
begin begin
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
ApplyRowValidation(idx); ApplyRowValidation(idx);
ShowRowValidationMessage(idx, 'Complete required fields before leaving this row.'); ShowRowValidationMessage(idx, 'Complete required fields before leaving this row.');
SetTopControlsEnabled(False); SetTopControlsEnabled(False);
...@@ -721,9 +725,6 @@ begin ...@@ -721,9 +725,6 @@ begin
ClearRowValidation(idx); ClearRowValidation(idx);
SetTopControlsEnabled(True); SetTopControlsEnabled(True);
if Assigned(rowEl.querySelector('[data-unsaved-data="1"]')) then
SaveRow(idx);
end, end,
0 0
); );
...@@ -770,6 +771,11 @@ begin ...@@ -770,6 +771,11 @@ begin
xdwdsTimeEntriestaskId.AsString := newValue; xdwdsTimeEntriestaskId.AsString := newValue;
xdwdsTimeEntriestaskDisplay.AsString := newDisplay; xdwdsTimeEntriestaskDisplay.AsString := newDisplay;
end end
else if SameText(fieldName, 'place') then
begin
xdwdsTimeEntriesplace.AsString := newValue;
xdwdsTimeEntriesplaceDesc.AsString := newDisplay;
end
else if SameText(fieldName, 'category') then else if SameText(fieldName, 'category') then
begin begin
xdwdsTimeEntriescategory.AsString := newValue; xdwdsTimeEntriescategory.AsString := newValue;
...@@ -781,13 +787,19 @@ begin ...@@ -781,13 +787,19 @@ begin
xdwdsTimeEntries.Post; xdwdsTimeEntries.Post;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
btn := TJSHTMLElement(document.getElementById(triggerId)); btn := TJSHTMLElement(document.getElementById(triggerId));
btn.setAttribute('data-unsaved-data', '1'); if Assigned(btn) then
begin
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label')); labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label'));
if Assigned(labelEl) then
labelEl.textContent := newDisplay; labelEl.textContent := newDisplay;
btn.focus; btn.focus;
end;
SaveField(idx, fieldName);
if ValidateRow(idx) then if ValidateRow(idx) then
begin begin
...@@ -853,11 +865,21 @@ begin ...@@ -853,11 +865,21 @@ begin
end; end;
FActiveRowIndex := idx; FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
SetTopControlsEnabled(False); SetTopControlsEnabled(False);
ApplyActiveRowState;
firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="taskId"]'));
if not Assigned(firstEditor) then
firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="hours"]'));
firstEditor := TJSHTMLElement(rowEl.querySelector('[data-field="taskDate"]'));
if Assigned(firstEditor) then if Assigned(firstEditor) then
firstEditor.focus; begin
asm
firstEditor.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
firstEditor.focus();
end;
end;
FPendingEntryId := ''; FPendingEntryId := '';
end; end;
...@@ -975,10 +997,11 @@ begin ...@@ -975,10 +997,11 @@ begin
el := ATarget; el := ATarget;
while Assigned(el) do while Assigned(el) do
begin begin
idxText := string(el.getAttribute('data-idx')); if el.hasAttribute('data-idx') then
if idxText <> '' then
begin begin
idxText := string(el.getAttribute('data-idx'));
Result := StrToIntDef(idxText, -1); Result := StrToIntDef(idxText, -1);
if Result >= 0 then if Result >= 0 then
Exit; Exit;
end; end;
...@@ -989,8 +1012,32 @@ end; ...@@ -989,8 +1012,32 @@ end;
procedure TFTimeEntries.DocumentMouseDown(Event: TJSEvent); procedure TFTimeEntries.DocumentMouseDown(Event: TJSEvent);
var
targetEl: TJSHTMLElement;
targetRowIndex: Integer;
begin begin
FLastMouseDownRowIndex := GetTargetRowIndex(TJSHTMLElement(Event.target)); targetEl := TJSHTMLElement(Event.target);
targetRowIndex := GetTargetRowIndex(targetEl);
if (FActiveRowIndex >= 0) and
(targetRowIndex >= 0) and
(targetRowIndex <> FActiveRowIndex) and
(not ValidateRow(FActiveRowIndex)) then
begin
Event.preventDefault;
Event.stopPropagation;
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before leaving this row.');
SetTopControlsEnabled(False);
FLastMouseDownRowIndex := FActiveRowIndex;
FLastMouseDownTime := TJSDate.now;
ApplyActiveRowState;
Exit;
end;
FLastMouseDownRowIndex := targetRowIndex;
FLastMouseDownTime := TJSDate.now; FLastMouseDownTime := TJSDate.now;
end; end;
...@@ -1055,7 +1102,7 @@ begin ...@@ -1055,7 +1102,7 @@ begin
if await(AddTimeEntry) then if await(AddTimeEntry) then
begin begin
CaptureTableScroll; CaptureTableScroll;
LoadTimeEntries; await(LoadTimeEntries);
end; end;
finally finally
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
...@@ -1063,6 +1110,52 @@ begin ...@@ -1063,6 +1110,52 @@ begin
end; end;
procedure TFTimeEntries.btnDeleteEntryClick(Sender: TObject);
begin
if FActiveRowIndex < 0 then
begin
Utils.ShowErrorModal('Select a time entry to delete.');
Exit;
end;
Utils.ShowConfirmationModal(
'Delete the selected time entry?',
'Delete',
'Cancel',
procedure(AConfirmed: Boolean)
begin
if AConfirmed then
DeleteSelectedEntry;
end
);
end;
[async] procedure TFTimeEntries.btnSearchRangeClick(Sender: TObject);
begin
if edtStartDate.Text = '' then
begin
Utils.ShowErrorModal('Select a start date.');
Exit;
end;
if edtEndDate.Text = '' then
begin
Utils.ShowErrorModal('Select an end date.');
Exit;
end;
if IsoToDate(edtEndDate.Text) < IsoToDate(edtStartDate.Text) then
begin
Utils.ShowErrorModal('End date cannot be before start date.');
Exit;
end;
FStartDate := edtStartDate.Text;
FEndDate := edtEndDate.Text;
await(LoadTimeEntries);
end;
procedure TFTimeEntries.CaptureTableScroll; procedure TFTimeEntries.CaptureTableScroll;
begin begin
asm asm
...@@ -1091,7 +1184,236 @@ begin ...@@ -1091,7 +1184,236 @@ begin
edtWeekOf.Enabled := AEnabled; edtWeekOf.Enabled := AEnabled;
edtStartDate.Enabled := AEnabled; edtStartDate.Enabled := AEnabled;
edtEndDate.Enabled := AEnabled; edtEndDate.Enabled := AEnabled;
btnSearchRange.Enabled := AEnabled;
btnAddEntry.Enabled := AEnabled; btnAddEntry.Enabled := AEnabled;
end; end;
[async] procedure TFTimeEntries.TaskPickerButtonClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
begin
Event.preventDefault;
Event.stopPropagation;
el := TJSHTMLElement(Event.currentTarget);
idx := StrToIntDef(string(el.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
if not Assigned(FTaskPickerOffCanvas) then
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
await(FTaskPickerOffCanvas.Open(FUserId, idx));
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 := True;
CaptureTableScroll;
RenderTable;
triggerId := 'time_dd_taskId_' + IntToStr(ARowIndex);
btn := TJSHTMLElement(document.getElementById(triggerId));
if Assigned(btn) then
btn.focus;
SaveField(ARowIndex, 'taskId');
if ValidateRow(ARowIndex) then
begin
ClearRowValidation(ARowIndex);
SetTopControlsEnabled(True);
end
else
SetTopControlsEnabled(False);
end;
procedure TFTimeEntries.ApplyActiveRowState;
begin
asm
const activeRowIndex = this.FActiveRowIndex;
document.querySelectorAll('.time-row-selectable').forEach(function(row) {
const rowIndex = parseInt(row.getAttribute('data-idx') || '-1', 10);
if ((activeRowIndex >= 0) && (rowIndex === activeRowIndex))
row.classList.add('table-active');
else
row.classList.remove('table-active');
});
end;
end;
procedure TFTimeEntries.EditorBlur(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr: string;
fieldName: string;
begin
el := TJSHTMLElement(Event.target);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
if string(el.getAttribute('data-unsaved-data')) <> '1' then
begin
console.log('EditorBlur: skip (not unsaved) idx=' + idxStr + ' field=' + fieldName);
Exit;
end;
el.removeAttribute('data-unsaved-data');
idx := StrToIntDef(idxStr, -1);
if idx < 0 then
Exit;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
SaveField(idx, fieldName);
end;
[async] procedure TFTimeEntries.SaveField(AIndex: Integer; const AFieldName: string);
var
response: TXDataClientResponse;
payload: TJSObject;
fieldValue: string;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
if xdwdsTimeEntriesentryId.AsInteger <= 0 then
Exit;
fieldValue := '';
if SameText(AFieldName, 'taskDate') then
fieldValue := xdwdsTimeEntriestaskDate.AsString
else if SameText(AFieldName, 'taskId') then
fieldValue := xdwdsTimeEntriestaskId.AsString
else if SameText(AFieldName, 'hours') then
begin
if xdwdsTimeEntrieshours.IsNull then
fieldValue := ''
else
fieldValue := xdwdsTimeEntrieshours.AsString;
end
else if SameText(AFieldName, 'taskTime') then
fieldValue := xdwdsTimeEntriestaskTime.AsString
else if SameText(AFieldName, 'place') then
fieldValue := xdwdsTimeEntriesplace.AsString
else if SameText(AFieldName, 'category') then
fieldValue := xdwdsTimeEntriescategory.AsString
else if SameText(AFieldName, 'summary') then
fieldValue := xdwdsTimeEntriessummary.AsString
else
Exit;
payload := TJSObject.new;
payload['entryId'] := xdwdsTimeEntriesentryId.AsInteger;
payload['userId'] := FUserId;
payload['fieldName'] := AFieldName;
payload['value'] := fieldValue;
console.log('SaveField: idx=' + IntToStr(AIndex) + ' field=' + AFieldName + ' value=' + fieldValue);
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.SaveTimeEntryField',
[payload]
));
console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result)));
except
on E: EXDataClientRequestException do
begin
console.log('SaveField ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
[async] procedure TFTimeEntries.DeleteSelectedEntry;
var
entryId: Integer;
begin
if not xdwdsTimeEntries.Active then
Exit;
if FActiveRowIndex < 0 then
begin
Utils.ShowErrorModal('Select a time entry to delete.');
Exit;
end;
GotoRowIndex(FActiveRowIndex);
if xdwdsTimeEntries.Eof then
Exit;
entryId := xdwdsTimeEntriesentryId.AsInteger;
if entryId <= 0 then
begin
Utils.ShowErrorModal('Unable to determine the selected time entry.');
Exit;
end;
Utils.ShowSpinner('spinner');
try
try
await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.DeleteTimeEntry',
[FUserId, entryId]
));
except
on E: EXDataClientRequestException do
begin
console.log('DeleteSelectedEntry ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
Exit;
end;
end;
FActiveRowIndex := -1;
FPendingEntryId := '';
HideRowValidationMessage;
SetTopControlsEnabled(True);
btnDeleteEntry.Enabled := False;
CaptureTableScroll;
await(LoadTimeEntries);
finally
Utils.HideSpinner('spinner');
end;
end;
end. end.
...@@ -17,7 +17,8 @@ uses ...@@ -17,7 +17,8 @@ uses
uDropdownHelpers in 'uDropdownHelpers.pas', uDropdownHelpers in 'uDropdownHelpers.pas',
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html}, View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html}, View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.TimeEntries in 'View.TimeEntries.pas' {FTimeEntries: TWebForm} {*.html}; View.TimeEntries in 'View.TimeEntries.pas' {FTimeEntries: TWebForm} {*.html},
uTaskPickerOffCanvas in 'uTaskPickerOffCanvas.pas';
{$R *.res} {$R *.res}
......
...@@ -156,6 +156,7 @@ ...@@ -156,6 +156,7 @@
<Form>FTimeEntries</Form> <Form>FTimeEntries</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="uTaskPickerOffCanvas.pas"/>
<None Include="index.html"/> <None Include="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="config\config.json"/> <None Include="config\config.json"/>
......
unit uTaskPickerOffCanvas;
interface
uses
System.SysUtils, JS, Web, XData.Web.Client;
type
TTaskPickedProc = reference to procedure(ARowIndex: Integer; const ATaskId, ATaskDisplay: string);
TTaskPickerOffCanvas = class
private
FClient: TXDataWebClient;
FUserId: string;
FRowIndex: Integer;
FOnTaskPicked: TTaskPickedProc;
FCustomers: TJSArray;
FProjects: TJSArray;
FTasks: TJSArray;
procedure BindEvents;
procedure ClearSelect(const AElementId: string; ADisabled: Boolean);
procedure FillSelect(const AElementId: string; AItems: TJSArray);
function SelectedValue(const AElementId: string): string;
function FindDisplay(AItems: TJSArray; const AValue: string): string;
procedure SetStatus(const AMessage: string);
procedure ClearStatus;
procedure SetSaveEnabled(AEnabled: Boolean);
procedure Show;
procedure Hide;
[async] procedure LoadCustomers;
[async] procedure LoadProjects(const ACustomerId: string);
[async] procedure LoadTasks(const AProjectId: string);
[async] procedure CustomerChanged(Event: TJSEvent);
[async] procedure ProjectChanged(Event: TJSEvent);
procedure TaskChanged(Event: TJSEvent);
procedure SaveClicked(Event: TJSEvent);
public
constructor Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc);
[async] procedure Open(const AUserId: string; ARowIndex: Integer);
end;
implementation
constructor TTaskPickerOffCanvas.Create(AClient: TXDataWebClient; AOnTaskPicked: TTaskPickedProc);
begin
inherited Create;
FClient := AClient;
FOnTaskPicked := AOnTaskPicked;
FCustomers := TJSArray.new;
FProjects := TJSArray.new;
FTasks := TJSArray.new;
FRowIndex := -1;
BindEvents;
end;
procedure TTaskPickerOffCanvas.BindEvents;
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('task_picker_customer'));
if Assigned(el) then
el.addEventListener('change', TJSEventHandler(@CustomerChanged));
el := TJSHTMLElement(document.getElementById('task_picker_project'));
if Assigned(el) then
el.addEventListener('change', TJSEventHandler(@ProjectChanged));
el := TJSHTMLElement(document.getElementById('task_picker_task'));
if Assigned(el) then
el.addEventListener('change', TJSEventHandler(@TaskChanged));
el := TJSHTMLElement(document.getElementById('btn_task_picker_save'));
if Assigned(el) then
el.addEventListener('click', TJSEventHandler(@SaveClicked));
end;
procedure TTaskPickerOffCanvas.ClearSelect(const AElementId: string; ADisabled: Boolean);
var
selectEl: TJSHTMLSelectElement;
begin
selectEl := TJSHTMLSelectElement(document.getElementById(AElementId));
if not Assigned(selectEl) then
Exit;
selectEl.innerHTML := '<option value=""></option>';
selectEl.disabled := ADisabled;
end;
procedure TTaskPickerOffCanvas.FillSelect(const AElementId: string; AItems: TJSArray);
var
selectEl: TJSHTMLSelectElement;
optionEl: TJSHTMLOptionElement;
itemObj: TJSObject;
i: Integer;
begin
selectEl := TJSHTMLSelectElement(document.getElementById(AElementId));
if not Assigned(selectEl) then
Exit;
selectEl.innerHTML := '<option value=""></option>';
for i := 0 to AItems.length - 1 do
begin
itemObj := TJSObject(AItems[i]);
optionEl := TJSHTMLOptionElement(document.createElement('option'));
optionEl.value := string(itemObj['value']);
optionEl.text := string(itemObj['display']);
selectEl.appendChild(optionEl);
end;
selectEl.disabled := False;
end;
function TTaskPickerOffCanvas.SelectedValue(const AElementId: string): string;
var
selectEl: TJSHTMLSelectElement;
begin
Result := '';
selectEl := TJSHTMLSelectElement(document.getElementById(AElementId));
if Assigned(selectEl) then
Result := string(selectEl.value);
end;
function TTaskPickerOffCanvas.FindDisplay(AItems: TJSArray; const AValue: string): string;
var
itemObj: TJSObject;
i: Integer;
begin
Result := '';
for i := 0 to AItems.length - 1 do
begin
itemObj := TJSObject(AItems[i]);
if string(itemObj['value']) = AValue then
Exit(string(itemObj['display']));
end;
end;
procedure TTaskPickerOffCanvas.SetStatus(const AMessage: string);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('task_picker_status'));
if not Assigned(el) then
Exit;
el.innerText := AMessage;
el.classList.remove('d-none');
end;
procedure TTaskPickerOffCanvas.ClearStatus;
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('task_picker_status'));
if not Assigned(el) then
Exit;
el.innerText := '';
el.classList.add('d-none');
end;
procedure TTaskPickerOffCanvas.SetSaveEnabled(AEnabled: Boolean);
var
btn: TJSHTMLButtonElement;
begin
btn := TJSHTMLButtonElement(document.getElementById('btn_task_picker_save'));
if Assigned(btn) then
btn.disabled := not AEnabled;
end;
procedure TTaskPickerOffCanvas.Show;
begin
asm
const el = document.getElementById('task_picker_offcanvas');
if (!el || !window.bootstrap) return;
const oc = bootstrap.Offcanvas.getOrCreateInstance(el);
oc.show();
end;
end;
procedure TTaskPickerOffCanvas.Hide;
begin
asm
const el = document.getElementById('task_picker_offcanvas');
if (!el || !window.bootstrap) return;
const oc = bootstrap.Offcanvas.getOrCreateInstance(el);
oc.hide();
end;
end;
[async] procedure TTaskPickerOffCanvas.LoadCustomers;
var
response: TXDataClientResponse;
resultObj: TJSObject;
begin
response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerCustomers',
[FUserId]
));
resultObj := TJSObject(response.Result);
FCustomers := TJSArray(resultObj['options']);
FillSelect('task_picker_customer', FCustomers);
end;
[async] procedure TTaskPickerOffCanvas.LoadProjects(const ACustomerId: string);
var
response: TXDataClientResponse;
resultObj: TJSObject;
begin
response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerProjects',
[FUserId, ACustomerId]
));
resultObj := TJSObject(response.Result);
FProjects := TJSArray(resultObj['options']);
FillSelect('task_picker_project', FProjects);
end;
[async] procedure TTaskPickerOffCanvas.LoadTasks(const AProjectId: string);
var
response: TXDataClientResponse;
resultObj: TJSObject;
begin
response := await(FClient.RawInvokeAsync(
'ITimeEntryService.GetTaskPickerTasks',
[FUserId, AProjectId]
));
resultObj := TJSObject(response.Result);
FTasks := TJSArray(resultObj['options']);
FillSelect('task_picker_task', FTasks);
end;
[async] procedure TTaskPickerOffCanvas.CustomerChanged(Event: TJSEvent);
var
customerId: string;
begin
ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True);
customerId := SelectedValue('task_picker_customer');
if customerId = '' then
Exit;
try
await(LoadProjects(customerId));
except
on E: Exception do
SetStatus('Unable to load projects.');
end;
end;
[async] procedure TTaskPickerOffCanvas.ProjectChanged(Event: TJSEvent);
var
projectId: string;
begin
ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_task', True);
projectId := SelectedValue('task_picker_project');
if projectId = '' then
Exit;
try
await(LoadTasks(projectId));
except
on E: Exception do
SetStatus('Unable to load tasks.');
end;
end;
procedure TTaskPickerOffCanvas.TaskChanged(Event: TJSEvent);
begin
ClearStatus;
SetSaveEnabled(SelectedValue('task_picker_task') <> '');
end;
procedure TTaskPickerOffCanvas.SaveClicked(Event: TJSEvent);
var
taskId: string;
taskDisplay: string;
begin
Event.preventDefault;
taskId := SelectedValue('task_picker_task');
if taskId = '' then
begin
SetStatus('Select a task.');
Exit;
end;
taskDisplay := FindDisplay(FTasks, taskId);
if Assigned(FOnTaskPicked) then
FOnTaskPicked(FRowIndex, taskId, taskDisplay);
Hide;
end;
[async] procedure TTaskPickerOffCanvas.Open(const AUserId: string; ARowIndex: Integer);
begin
FUserId := AUserId;
FRowIndex := ARowIndex;
ClearStatus;
SetSaveEnabled(False);
ClearSelect('task_picker_customer', True);
ClearSelect('task_picker_project', True);
ClearSelect('task_picker_task', True);
Show;
try
await(LoadCustomers);
except
on E: Exception do
SetStatus('Unable to load customers.');
end;
end;
end.
...@@ -6,6 +6,9 @@ object ApiDatabase: TApiDatabase ...@@ -6,6 +6,9 @@ object ApiDatabase: TApiDatabase
AutoCommit = False AutoCommit = False
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root'
Server = '192.168.102.131'
Connected = True
LoginPrompt = False LoginPrompt = False
Left = 435 Left = 435
Top = 359 Top = 359
...@@ -1149,8 +1152,14 @@ object ApiDatabase: TApiDatabase ...@@ -1149,8 +1152,14 @@ object ApiDatabase: TApiDatabase
'SET' 'SET'
' TASK_DATE = :TASK_DATE,' ' TASK_DATE = :TASK_DATE,'
' TASK_ID = :TASK_ID,' ' TASK_ID = :TASK_ID,'
' PROJECT_ID = ('
' SELECT t.PROJECT_ID'
' FROM tasks t'
' WHERE t.TASK_ID = :TASK_ID'
' ),'
' HOURS = :HOURS,' ' HOURS = :HOURS,'
' TASK_TIME = :TASK_TIME,' ' TASK_TIME = :TASK_TIME,'
' PLACE = :PLACE,'
' CATEGORY = :CATEGORY,' ' CATEGORY = :CATEGORY,'
' SUMMARY = :SUMMARY,' ' SUMMARY = :SUMMARY,'
' MODIFY_DATE = now(),' ' MODIFY_DATE = now(),'
...@@ -1182,6 +1191,11 @@ object ApiDatabase: TApiDatabase ...@@ -1182,6 +1191,11 @@ object ApiDatabase: TApiDatabase
end end
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'PLACE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY' Name = 'CATEGORY'
Value = nil Value = nil
end end
...@@ -1206,4 +1220,145 @@ object ApiDatabase: TApiDatabase ...@@ -1206,4 +1220,145 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqTaskPickerCustomers: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' c.CUSTOMER_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME'
'from user_project up'
'join project p on p.PROJECT_ID = up.PROJECT_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
'where up.USER_ID = :USER_ID'
'order by c.SHORT_NAME')
Left = 402
Top = 494
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
object uqTaskPickerCustomersCUSTOMER_ID: TStringField
FieldName = 'CUSTOMER_ID'
Size = 7
end
object uqTaskPickerCustomersCUSTOMER_SHORT_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME'
Size = 10
end
end
object uqTaskPickerProjects: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' p.PROJECT_ID,'
' p.NAME as PROJECT_NAME'
'from user_project up'
'join project p on p.PROJECT_ID = up.PROJECT_ID'
'where up.USER_ID = :USER_ID'
' and p.CUSTOMER_ID = :CUSTOMER_ID'
'order by p.NAME')
Left = 544
Top = 494
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'CUSTOMER_ID'
Value = nil
end>
object uqTaskPickerProjectsPROJECT_ID: TStringField
FieldName = 'PROJECT_ID'
Required = True
Size = 7
end
object uqTaskPickerProjectsPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME'
Size = 30
end
end
object uqTaskPickerTasks: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' t.TASK_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME,'
' p.NAME as PROJECT_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,'
' t.SUBJECT as TASK_SUBJECT'
'from tasks t'
'join project p on p.PROJECT_ID = t.PROJECT_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
'join user_project up on up.PROJECT_ID = p.PROJECT_ID'
'where up.USER_ID = :USER_ID'
' and t.PROJECT_ID = :PROJECT_ID'
'order by'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' t.SUBJECT')
Left = 676
Top = 502
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'PROJECT_ID'
Value = nil
end>
object uqTaskPickerTasksTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqTaskPickerTasksCUSTOMER_SHORT_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME'
ReadOnly = True
Size = 10
end
object uqTaskPickerTasksPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME'
ReadOnly = True
Size = 30
end
object uqTaskPickerTasksTASK_NUM_1: TIntegerField
FieldName = 'TASK_NUM_1'
end
object uqTaskPickerTasksTASK_NUM_2: TIntegerField
FieldName = 'TASK_NUM_2'
end
object uqTaskPickerTasksTASK_NUM_3: TIntegerField
FieldName = 'TASK_NUM_3'
end
object uqTaskPickerTasksTASK_NUM_4: TIntegerField
FieldName = 'TASK_NUM_4'
end
object uqTaskPickerTasksTASK_NUM_5: TIntegerField
FieldName = 'TASK_NUM_5'
end
object uqTaskPickerTasksTASK_NUM_6: TIntegerField
FieldName = 'TASK_NUM_6'
end
object uqTaskPickerTasksTASK_SUBJECT: TStringField
FieldName = 'TASK_SUBJECT'
Size = 80
end
end
end end
...@@ -90,6 +90,23 @@ type ...@@ -90,6 +90,23 @@ type
uqProjectReportedUsersNAME: TStringField; uqProjectReportedUsersNAME: TStringField;
uqAddTimeEntry: TUniQuery; uqAddTimeEntry: TUniQuery;
uqSaveTimeEntry: TUniQuery; uqSaveTimeEntry: TUniQuery;
uqTaskPickerCustomers: TUniQuery;
uqTaskPickerProjects: TUniQuery;
uqTaskPickerTasks: TUniQuery;
uqTaskPickerCustomersCUSTOMER_ID: TStringField;
uqTaskPickerCustomersCUSTOMER_SHORT_NAME: TStringField;
uqTaskPickerProjectsPROJECT_ID: TStringField;
uqTaskPickerProjectsPROJECT_NAME: TStringField;
uqTaskPickerTasksTASK_ID: TStringField;
uqTaskPickerTasksCUSTOMER_SHORT_NAME: TStringField;
uqTaskPickerTasksPROJECT_NAME: TStringField;
uqTaskPickerTasksTASK_NUM_1: TIntegerField;
uqTaskPickerTasksTASK_NUM_2: TIntegerField;
uqTaskPickerTasksTASK_NUM_3: TIntegerField;
uqTaskPickerTasksTASK_NUM_4: TIntegerField;
uqTaskPickerTasksTASK_NUM_5: TIntegerField;
uqTaskPickerTasksTASK_NUM_6: TIntegerField;
uqTaskPickerTasksTASK_SUBJECT: TStringField;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -20,6 +20,8 @@ type ...@@ -20,6 +20,8 @@ type
taskDisplay: string; taskDisplay: string;
hours: Nullable<Double>; hours: Nullable<Double>;
taskTime: string; taskTime: string;
place: string;
placeDesc: string;
category: string; category: string;
categoryDesc: string; categoryDesc: string;
summary: string; summary: string;
...@@ -44,6 +46,7 @@ type ...@@ -44,6 +46,7 @@ type
items: TList<TTimeEntry>; items: TList<TTimeEntry>;
taskOptions: TList<TTimeEntryTaskOption>; taskOptions: TList<TTimeEntryTaskOption>;
categoryOptions: TList<TTimeEntryCategoryOption>; categoryOptions: TList<TTimeEntryCategoryOption>;
placeOptions: TList<TTimeEntryCategoryOption>;
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
end; end;
...@@ -56,16 +59,45 @@ type ...@@ -56,16 +59,45 @@ type
taskId: string; taskId: string;
hours: Double; hours: Double;
taskTime: string; taskTime: string;
place: string;
category: string; category: string;
summary: string; summary: string;
end; end;
TTimeEntryFieldSave = class
public
entryId: Integer;
userId: string;
fieldName: string;
value: string;
end;
type
TTaskPickerOption = class
public
value: string;
display: string;
end;
TTaskPickerOptionsResponse = class
public
options: TList<TTaskPickerOption>;
constructor Create;
destructor Destroy; override;
end;
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
ITimeEntryService = interface(IInvokable) ITimeEntryService = interface(IInvokable)
['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'] ['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}']
[HttpGet] function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse; [HttpGet] function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
[HttpPost] function AddTimeEntry(userId, taskDate: string): string; [HttpPost] function AddTimeEntry(userId, taskDate: string): string;
[HttpPost] function SaveTimeEntry(Item: TTimeEntrySave): Boolean; [HttpPost] function SaveTimeEntry(Item: TTimeEntrySave): Boolean;
[HttpPost] function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
[HttpPost] function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse;
function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse;
end; end;
implementation implementation
...@@ -76,6 +108,7 @@ begin ...@@ -76,6 +108,7 @@ begin
items := TList<TTimeEntry>.Create; items := TList<TTimeEntry>.Create;
taskOptions := TList<TTimeEntryTaskOption>.Create; taskOptions := TList<TTimeEntryTaskOption>.Create;
categoryOptions := TList<TTimeEntryCategoryOption>.Create; categoryOptions := TList<TTimeEntryCategoryOption>.Create;
placeOptions := TList<TTimeEntryCategoryOption>.Create;
end; end;
destructor TTimeEntriesResponse.Destroy; destructor TTimeEntriesResponse.Destroy;
...@@ -83,6 +116,19 @@ begin ...@@ -83,6 +116,19 @@ begin
items.Free; items.Free;
taskOptions.Free; taskOptions.Free;
categoryOptions.Free; categoryOptions.Free;
placeOptions.Free;
inherited;
end;
constructor TTaskPickerOptionsResponse.Create;
begin
inherited;
options := TList<TTaskPickerOption>.Create;
end;
destructor TTaskPickerOptionsResponse.Destroy;
begin
options.Free;
inherited; inherited;
end; end;
......
...@@ -26,12 +26,18 @@ type ...@@ -26,12 +26,18 @@ type
procedure LoadUserName(AResponse: TTimeEntriesResponse; userId: string); procedure LoadUserName(AResponse: TTimeEntriesResponse; userId: string);
procedure LoadTimeEntryItems(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime); procedure LoadTimeEntryItems(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
procedure LoadTaskOptions(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime); procedure LoadTaskOptions(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
procedure LoadPlaceOptions(AResponse: TTimeEntriesResponse);
procedure LoadCategoryOptions(AResponse: TTimeEntriesResponse); procedure LoadCategoryOptions(AResponse: TTimeEntriesResponse);
function GetNextIdValue(const AKeyName: string): string; function GetNextIdValue(const AKeyName: string): string;
public public
function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse; function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
function AddTimeEntry(userId, taskDate: string): string; function AddTimeEntry(userId, taskDate: string): string;
function SaveTimeEntry(Item: TTimeEntrySave): Boolean; function SaveTimeEntry(Item: TTimeEntrySave): Boolean;
function SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
function GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse;
function GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse;
function GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse;
function DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
end; end;
implementation implementation
...@@ -101,6 +107,19 @@ begin ...@@ -101,6 +107,19 @@ begin
AddPart(FieldText(AQuery, 'TASK_NUM_6')); AddPart(FieldText(AQuery, 'TASK_NUM_6'));
end; end;
procedure AddTaskPickerOption(AResponse: TTaskPickerOptionsResponse; const AValue, ADisplay: string);
var
option: TTaskPickerOption;
begin
option := TTaskPickerOption.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(option);
option.value := AValue;
option.display := ADisplay;
AResponse.options.Add(option);
end;
function TTimeEntryService.BuildTaskDisplay(AQuery: TUniQuery): string; function TTimeEntryService.BuildTaskDisplay(AQuery: TUniQuery): string;
var var
...@@ -187,6 +206,7 @@ begin ...@@ -187,6 +206,7 @@ begin
' ti.TASK_ID, ' + ' ti.TASK_ID, ' +
' ti.HOURS, ' + ' ti.HOURS, ' +
' ti.TASK_TIME, ' + ' ti.TASK_TIME, ' +
' ti.PLACE, ' +
' ti.CATEGORY, ' + ' ti.CATEGORY, ' +
' cat.CODE_DESC as CATEGORY_DESC, ' + ' cat.CODE_DESC as CATEGORY_DESC, ' +
' ti.SUMMARY, ' + ' ti.SUMMARY, ' +
...@@ -198,12 +218,14 @@ begin ...@@ -198,12 +218,14 @@ begin
' 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 as TASK_SUBJECT ' + ' t.SUBJECT as TASK_SUBJECT, ' +
' plc.CODE_DESC as PLACE_DESC ' +
'from time_items ti ' + 'from time_items ti ' +
'left join tasks t on t.TASK_ID = ti.TASK_ID ' + 'left join tasks t on t.TASK_ID = ti.TASK_ID ' +
'left join project p on p.PROJECT_ID = t.PROJECT_ID ' + 'left join project p on p.PROJECT_ID = t.PROJECT_ID ' +
'left join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID ' + 'left join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID ' +
'left join codes cat on cat.CATEGORY = ''TASK'' and cat.CODE_TYPE = ''CATGRY'' and cat.CODE = ti.CATEGORY ' + 'left join codes cat on cat.CATEGORY = ''TASK'' and cat.CODE_TYPE = ''CATGRY'' and cat.CODE = ti.CATEGORY ' +
'left join codes plc on plc.CATEGORY = ''GENERAL'' and plc.CODE_TYPE = ''PLACE'' and plc.CODE = ti.PLACE ' +
'where ti.USER_ID = :USER_ID ' + 'where ti.USER_ID = :USER_ID ' +
' and ti.TASK_DATE >= :START_DATE ' + ' and ti.TASK_DATE >= :START_DATE ' +
' and ti.TASK_DATE < :END_DATE ' + ' and ti.TASK_DATE < :END_DATE ' +
...@@ -234,6 +256,12 @@ begin ...@@ -234,6 +256,12 @@ begin
item.taskTime := FieldText(itemsQuery, 'TASK_TIME'); item.taskTime := FieldText(itemsQuery, 'TASK_TIME');
item.place := FieldText(itemsQuery, 'PLACE');
item.placeDesc := FieldText(itemsQuery, 'PLACE_DESC');
if item.placeDesc = '' then
item.placeDesc := item.place;
item.category := FieldText(itemsQuery, 'CATEGORY'); item.category := FieldText(itemsQuery, 'CATEGORY');
item.categoryDesc := FieldText(itemsQuery, 'CATEGORY_DESC'); item.categoryDesc := FieldText(itemsQuery, 'CATEGORY_DESC');
...@@ -271,32 +299,15 @@ begin ...@@ -271,32 +299,15 @@ begin
' t.TASK_NUM_5, ' + ' t.TASK_NUM_5, ' +
' t.TASK_NUM_6, ' + ' t.TASK_NUM_6, ' +
' t.SUBJECT as TASK_SUBJECT ' + ' t.SUBJECT as TASK_SUBJECT ' +
'from tasks t ' + 'from task_assigned_user tau ' +
'join tasks t on t.TASK_ID = tau.TASK_ID ' +
'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 ' +
'where ( ' + 'where tau.USER_ID = :USER_ID ' +
' ( ' + ' and upper(trim(coalesce(tau.FOCUSED_TASK, ''''))) = ''T'' ' +
' t.ASSIGNED_TO = :USER_ID ' +
' and t.FOCUS = ''T'' ' +
' and (t.START_DATE <= :ACTIVE_DATE or t.START_DATE is null) ' +
' and (t.COMPLETION_DATE >= :ACTIVE_DATE or t.COMPLETION_DATE is null) ' +
' ) ' +
' or t.TASK_ID in ( ' +
' select distinct ti.TASK_ID ' +
' from time_items ti ' +
' where ti.USER_ID = :USER_ID_EXISTING ' +
' and ti.TASK_DATE >= :START_DATE ' +
' and ti.TASK_DATE < :END_DATE ' +
' and ti.TASK_ID is not null ' +
' ) ' +
') ' +
'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'; '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.ParamByName('USER_ID').AsString := userId;
tasksQuery.ParamByName('USER_ID_EXISTING').AsString := userId;
tasksQuery.ParamByName('ACTIVE_DATE').AsDateTime := startDateValue;
tasksQuery.ParamByName('START_DATE').AsDateTime := startDateValue;
tasksQuery.ParamByName('END_DATE').AsDateTime := exclusiveEndDateValue;
tasksQuery.Open; tasksQuery.Open;
while not tasksQuery.Eof do while not tasksQuery.Eof do
...@@ -350,6 +361,40 @@ begin ...@@ -350,6 +361,40 @@ begin
end; end;
procedure TTimeEntryService.LoadPlaceOptions(AResponse: TTimeEntriesResponse);
var
placeQuery: TUniQuery;
placeOption: TTimeEntryCategoryOption;
begin
placeQuery := TUniQuery.Create(nil);
try
placeQuery.Connection := apiDB.ucETaskApi;
placeQuery.SQL.Text :=
'select CODE, CODE_DESC ' +
'from codes ' +
'where CATEGORY = ''GENERAL'' ' +
' and CODE_TYPE = ''PLACE'' ' +
'order by CODE_DESC';
placeQuery.Open;
while not placeQuery.Eof do
begin
placeOption := TTimeEntryCategoryOption.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(placeOption);
placeOption.code := FieldText(placeQuery, 'CODE');
placeOption.codeDesc := FieldText(placeQuery, 'CODE_DESC');
AResponse.placeOptions.Add(placeOption);
placeQuery.Next;
end;
finally
placeQuery.Free;
end;
end;
function TTimeEntryService.GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse; function TTimeEntryService.GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
var var
startDateValue: TDateTime; startDateValue: TDateTime;
...@@ -368,6 +413,7 @@ begin ...@@ -368,6 +413,7 @@ begin
LoadUserName(Result, userId); LoadUserName(Result, userId);
LoadTimeEntryItems(Result, userId, startDateValue, exclusiveEndDateValue); LoadTimeEntryItems(Result, userId, startDateValue, exclusiveEndDateValue);
LoadTaskOptions(Result, userId, startDateValue, exclusiveEndDateValue); LoadTaskOptions(Result, userId, startDateValue, exclusiveEndDateValue);
LoadPlaceOptions(Result);
LoadCategoryOptions(Result); LoadCategoryOptions(Result);
Result.count := Result.items.Count; Result.count := Result.items.Count;
...@@ -433,13 +479,31 @@ end; ...@@ -433,13 +479,31 @@ end;
function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): Boolean; function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): Boolean;
var var
taskDateValue: TDateTime; taskDateValue: TDateTime;
entryId: string;
isNewEntry: Boolean;
begin begin
Logger.Log(4, Format('TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"', [Item.entryId, Item.userId])); Logger.Log(4, Format('TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"', [Item.entryId, Item.userId]));
taskDateValue := ParseIsoDate(Item.taskDate); taskDateValue := ParseIsoDate(Item.taskDate);
isNewEntry := StrToIntDef(Item.entryId, 0) <= 0;
if isNewEntry then
begin
entryId := GetNextIdValue('TimeEntryId');
apiDB.uqAddTimeEntry.Close;
apiDB.uqAddTimeEntry.ParamByName('ENTRY_ID').AsString := entryId;
apiDB.uqAddTimeEntry.ParamByName('USER_ID').AsString := Item.userId;
apiDB.uqAddTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqAddTimeEntry.ParamByName('CREATED_BY').AsString := Item.userId;
apiDB.uqAddTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId;
apiDB.uqAddTimeEntry.ExecSQL;
end
else
entryId := Item.entryId;
apiDB.uqSaveTimeEntry.Close; apiDB.uqSaveTimeEntry.Close;
apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := Item.entryId; apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := entryId;
apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId; apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue; apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId; apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId;
...@@ -450,6 +514,11 @@ begin ...@@ -450,6 +514,11 @@ begin
else else
apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').AsString := Item.taskTime; apiDB.uqSaveTimeEntry.ParamByName('TASK_TIME').AsString := Item.taskTime;
if Trim(Item.place) = '' then
apiDB.uqSaveTimeEntry.ParamByName('PLACE').Clear
else
apiDB.uqSaveTimeEntry.ParamByName('PLACE').AsString := Item.place;
apiDB.uqSaveTimeEntry.ParamByName('CATEGORY').AsString := Item.category; apiDB.uqSaveTimeEntry.ParamByName('CATEGORY').AsString := Item.category;
apiDB.uqSaveTimeEntry.ParamByName('SUMMARY').AsString := Item.summary; apiDB.uqSaveTimeEntry.ParamByName('SUMMARY').AsString := Item.summary;
apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId; apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId;
...@@ -458,7 +527,286 @@ begin ...@@ -458,7 +527,286 @@ begin
Result := True; Result := True;
Logger.Log(4, 'TimeEntryService.SaveTimeEntry - saved ENTRY_ID=' + Item.entryId); Logger.Log(4, 'TimeEntryService.SaveTimeEntry - saved ENTRY_ID=' + entryId);
end;
function TTimeEntryService.SaveTimeEntryField(Item: TTimeEntryFieldSave): Boolean;
var
uqSaveField: TUniQuery;
columnName: string;
d: TDateTime;
function MapFieldNameToColumn(AFieldName: string): string;
begin
Result := '';
if SameText(AFieldName, 'taskDate') then
Result := 'TASK_DATE'
else if SameText(AFieldName, 'hours') then
Result := 'HOURS'
else if SameText(AFieldName, 'taskTime') then
Result := 'TASK_TIME'
else if SameText(AFieldName, 'place') then
Result := 'PLACE'
else if SameText(AFieldName, 'category') then
Result := 'CATEGORY'
else if SameText(AFieldName, 'summary') then
Result := 'SUMMARY';
end;
function ParseDateOrZero(const S: string; out ADate: TDateTime): Boolean;
var
y: Integer;
m: Integer;
dInt: Integer;
begin
Result := False;
ADate := 0;
if Length(Trim(S)) <> 10 then
Exit;
y := StrToIntDef(Copy(S, 1, 4), 0);
m := StrToIntDef(Copy(S, 6, 2), 0);
dInt := StrToIntDef(Copy(S, 9, 2), 0);
if (y > 0) and (m > 0) and (dInt > 0) then
begin
try
ADate := EncodeDate(y, m, dInt);
Result := True;
except
Result := False;
end;
end;
end;
begin
Result := False;
if not Assigned(Item) then
raise Exception.Create('SaveTimeEntryField: Item is nil.');
if Item.entryId <= 0 then
raise Exception.Create('SaveTimeEntryField: Invalid entryId.');
if Trim(Item.userId) = '' then
raise Exception.Create('SaveTimeEntryField: Invalid userId.');
uqSaveField := TUniQuery.Create(nil);
try
uqSaveField.Connection := apiDB.ucETaskApi;
if SameText(Item.fieldName, 'taskId') then
begin
if Trim(Item.value) = '' then
begin
uqSaveField.SQL.Text :=
'update time_items ' +
'set TASK_ID = null, ' +
' PROJECT_ID = null, ' +
' MODIFY_DATE = now(), ' +
' MODIFIED_BY = :MODIFIED_BY ' +
'where ENTRY_ID = :ENTRY_ID ' +
' and USER_ID = :USER_ID';
end
else
begin
uqSaveField.SQL.Text :=
'update time_items ' +
'set TASK_ID = :TASK_ID, ' +
' PROJECT_ID = ( ' +
' select t.PROJECT_ID ' +
' from tasks t ' +
' where t.TASK_ID = :TASK_ID ' +
' ), ' +
' MODIFY_DATE = now(), ' +
' MODIFIED_BY = :MODIFIED_BY ' +
'where ENTRY_ID = :ENTRY_ID ' +
' and USER_ID = :USER_ID';
uqSaveField.ParamByName('TASK_ID').AsString := Item.value;
end;
end
else
begin
columnName := MapFieldNameToColumn(Item.fieldName);
if columnName = '' then
raise Exception.Create('SaveTimeEntryField: Invalid field name: ' + Item.fieldName);
uqSaveField.SQL.Text :=
'update time_items ' +
'set ' + columnName + ' = :' + columnName + ', ' +
' MODIFY_DATE = now(), ' +
' MODIFIED_BY = :MODIFIED_BY ' +
'where ENTRY_ID = :ENTRY_ID ' +
' and USER_ID = :USER_ID';
if SameText(Item.fieldName, 'taskDate') then
begin
if ParseDateOrZero(Item.value, d) then
uqSaveField.ParamByName(columnName).AsDateTime := d
else
uqSaveField.ParamByName(columnName).Clear;
end
else if SameText(Item.fieldName, 'hours') then
begin
if Trim(Item.value) = '' then
uqSaveField.ParamByName(columnName).Clear
else
uqSaveField.ParamByName(columnName).AsFloat := StrToFloatDef(Item.value, 0);
end
else
begin
if Trim(Item.value) = '' then
uqSaveField.ParamByName(columnName).Clear
else
uqSaveField.ParamByName(columnName).AsString := Item.value;
end;
end;
uqSaveField.ParamByName('ENTRY_ID').AsInteger := Item.entryId;
uqSaveField.ParamByName('USER_ID').AsString := Item.userId;
uqSaveField.ParamByName('MODIFIED_BY').AsString := Item.userId;
uqSaveField.ExecSQL;
Result := True;
Logger.Log(4, Format(
'TimeEntryService.SaveTimeEntryField - ENTRY_ID="%d" FIELD="%s"',
[Item.entryId, Item.fieldName]
));
finally
uqSaveField.Free;
end;
end;
function TTimeEntryService.GetTaskPickerCustomers(userId: string): TTaskPickerOptionsResponse;
begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerCustomers - USER_ID="%s"', [userId]));
Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerCustomers.Close;
apiDB.uqTaskPickerCustomers.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerCustomers.Open;
try
while not apiDB.uqTaskPickerCustomers.Eof do
begin
AddTaskPickerOption(
Result,
FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_ID'),
FieldText(apiDB.uqTaskPickerCustomers, 'CUSTOMER_SHORT_NAME')
);
apiDB.uqTaskPickerCustomers.Next;
end;
finally
apiDB.uqTaskPickerCustomers.Close;
end;
end;
function TTimeEntryService.GetTaskPickerProjects(userId, customerId: string): TTaskPickerOptionsResponse;
begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerProjects - USER_ID="%s" CUSTOMER_ID="%s"', [userId, customerId]));
Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerProjects.Close;
apiDB.uqTaskPickerProjects.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerProjects.ParamByName('CUSTOMER_ID').AsString := customerId;
apiDB.uqTaskPickerProjects.Open;
try
while not apiDB.uqTaskPickerProjects.Eof do
begin
AddTaskPickerOption(
Result,
FieldText(apiDB.uqTaskPickerProjects, 'PROJECT_ID'),
FieldText(apiDB.uqTaskPickerProjects, 'PROJECT_NAME')
);
apiDB.uqTaskPickerProjects.Next;
end;
finally
apiDB.uqTaskPickerProjects.Close;
end;
end;
function TTimeEntryService.GetTaskPickerTasks(userId, projectId: string): TTaskPickerOptionsResponse;
begin
Logger.Log(4, Format('TimeEntryService.GetTaskPickerTasks - USER_ID="%s" PROJECT_ID="%s"', [userId, projectId]));
Result := TTaskPickerOptionsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
apiDB.uqTaskPickerTasks.Close;
apiDB.uqTaskPickerTasks.ParamByName('USER_ID').AsString := userId;
apiDB.uqTaskPickerTasks.ParamByName('PROJECT_ID').AsString := projectId;
apiDB.uqTaskPickerTasks.Open;
try
while not apiDB.uqTaskPickerTasks.Eof do
begin
AddTaskPickerOption(
Result,
FieldText(apiDB.uqTaskPickerTasks, 'TASK_ID'),
BuildTaskDisplay(apiDB.uqTaskPickerTasks)
);
apiDB.uqTaskPickerTasks.Next;
end;
finally
apiDB.uqTaskPickerTasks.Close;
end;
end;
function TTimeEntryService.DeleteTimeEntry(userId: string; entryId: Integer): Boolean;
var
q: TUniQuery;
begin
Result := False;
if Trim(userId) = '' then
raise Exception.Create('DeleteTimeEntry: Invalid userId.');
if entryId <= 0 then
raise Exception.Create('DeleteTimeEntry: Invalid entryId.');
Logger.Log(4, Format(
'TimeEntryService.DeleteTimeEntry - USER_ID="%s" ENTRY_ID="%d"',
[userId, entryId]
));
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
q.SQL.Text :=
'delete from time_items ' +
'where ENTRY_ID = :ENTRY_ID ' +
' and USER_ID = :USER_ID';
q.ParamByName('ENTRY_ID').AsInteger := entryId;
q.ParamByName('USER_ID').AsString := userId;
q.ExecSQL;
if q.RowsAffected <= 0 then
raise Exception.Create('Time entry was not found or does not belong to this user.');
Result := True;
Logger.Log(4, Format(
'TimeEntryService.DeleteTimeEntry - deleted ENTRY_ID="%d"',
[entryId]
));
finally
q.Free;
end;
end; end;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.8.9 webClientVersion=0.8.9
LogFileNum=222 LogFileNum=236
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.131
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment