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
var btnRight = document.getElementById('btn_confirm_right');
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 (btnLeft) btnLeft.innerText = leftLabel;
if (btnRight) btnRight.innerText = rightLabel;
......
......@@ -86,24 +86,28 @@
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="mdl_confirmation" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="lbl_confirmation_body">
Placeholder text
</div>
<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-secondary" id="btn_confirm_right">Confirm</button>
<!-- Confirmation Modal -->
<div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="main_modal_body">
Placeholder text
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-danger me-3" id="btn_confirm_left">
Delete
</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div class="modal fade" id="mdl_notification" tabindex="-1" aria-labelledby="lbl_notification_title"
......
object FTimeEntries: TFTimeEntries
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel
Left = 90
Top = 32
Width = 69
Width = 3
Height = 15
ElementID = 'lbl_validation_message'
ElementFont = efCSS
......@@ -26,7 +27,7 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtWeekOf'
WidthPercent = 100.000000000000000000
OnChange = edtWeekOfChange
OnExit = edtWeekOfExit
end
object edtStartDate: TWebEdit
Left = 245
......@@ -41,7 +42,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtStartDate'
WidthPercent = 100.000000000000000000
OnChange = edtStartDateChange
end
object edtEndDate: TWebEdit
Left = 415
......@@ -56,7 +56,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtEndDate'
WidthPercent = 100.000000000000000000
OnChange = edtEndDateChange
end
object btnAddEntry: TWebButton
Left = 440
......@@ -72,6 +71,33 @@ object FTimeEntries: TFTimeEntries
WidthPercent = 100.000000000000000000
OnClick = btnAddEntryClick
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
Connection = DMConnection.ApiConnection
Left = 460
......@@ -107,5 +133,11 @@ object FTimeEntries: TFTimeEntries
object xdwdsTimeEntriessummary: TStringField
FieldName = 'summary'
end
object xdwdsTimeEntriesplace: TStringField
FieldName = 'place'
end
object xdwdsTimeEntriesplaceDesc: TStringField
FieldName = 'placeDesc'
end
end
end
......@@ -7,22 +7,74 @@
<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>
<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 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>
<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 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>
<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>
<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 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 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>
......@@ -4,8 +4,8 @@ interface
uses
System.SysUtils, System.Classes, System.DateUtils, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.ExtCtrls,
XData.Web.Client, ConnectionModule, XData.Web.JsonDataset, Data.DB, XData.Web.Dataset;
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, uTaskPickerOffCanvas;
type
TFTimeEntries = class(TWebForm)
......@@ -25,11 +25,15 @@ type
xdwdsTimeEntriescategoryDesc: TStringField;
btnAddEntry: TWebButton;
lblValidationMessage: TWebLabel;
procedure edtWeekOfChange(Sender: TObject);
procedure edtStartDateChange(Sender: TObject);
procedure edtEndDateChange(Sender: TObject);
xdwdsTimeEntriesplace: TStringField;
xdwdsTimeEntriesplaceDesc: TStringField;
btnDeleteEntry: TWebButton;
btnSearchRange: TWebButton;
procedure WebFormCreate(Sender: TObject);
[async] procedure btnAddEntryClick(Sender: TObject);
procedure btnDeleteEntryClick(Sender: TObject);
procedure edtWeekOfExit(Sender: TObject);
[async] procedure btnSearchRangeClick(Sender: TObject);
private
FUserId: string;
FUserName: string;
......@@ -37,6 +41,7 @@ type
FEndDate: string;
FTaskOptions: TJSArray;
FCategoryOptions: TJSArray;
FPlaceOptions: TJSArray;
FUpdatingDates: Boolean;
FPendingScrollTop: Integer;
FPendingScrollLeft: Integer;
......@@ -44,6 +49,7 @@ type
FPendingEntryId: string;
FLastMouseDownRowIndex: Integer;
FLastMouseDownTime: Double;
FTaskPickerOffCanvas: TTaskPickerOffCanvas;
[async] procedure LoadTimeEntries;
[async] function AddTimeEntry: Boolean;
procedure RenderTable;
......@@ -69,10 +75,15 @@ type
procedure ClearRowValidation(AIndex: Integer);
procedure ShowRowValidationMessage(AIndex: Integer; const AMessage: string);
procedure HideRowValidationMessage;
[async] procedure SaveRow(AIndex: Integer);
procedure SetTopControlsEnabled(AEnabled: Boolean);
function GetTargetRowIndex(ATarget: TJSHTMLElement): Integer;
procedure DocumentMouseDown(Event: TJSEvent);
[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
end;
......@@ -96,6 +107,7 @@ begin
FTaskOptions := TJSArray.new;
FCategoryOptions := TJSArray.new;
FPlaceOptions := TJSArray.new;
FUpdatingDates := False;
FPendingScrollTop := 0;
FPendingScrollLeft := 0;
......@@ -104,6 +116,8 @@ begin
FLastMouseDownRowIndex := -1;
FLastMouseDownTime := 0;
FTaskPickerOffCanvas := TTaskPickerOffCanvas.Create(xdwcTimeEntries, @TaskPickerTaskSelected);
document.addEventListener('mousedown', TJSEventHandler(@DocumentMouseDown));
payload := AuthService.TokenPayload;
......@@ -117,6 +131,7 @@ begin
end;
SetTimeEntriesLabel(FUserName);
btnDeleteEntry.Enabled := False;
anchorDate := Application.Parameters.Values['date'];
if anchorDate = '' then
......@@ -196,32 +211,19 @@ begin
LoadTimeEntries;
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
if FUpdatingDates then
Exit;
FStartDate := edtStartDate.Text;
LoadTimeEntries;
end;
procedure TFTimeEntries.edtEndDateChange(Sender: TObject);
begin
if FUpdatingDates then
if edtWeekOf.Text = '' then
Exit;
FEndDate := edtEndDate.Text;
LoadTimeEntries;
SetWeekRangeFromAnchor(edtWeekOf.Text, True);
end;
procedure TFTimeEntries.GotoRowIndex(AIndex: Integer);
var
i: Integer;
......@@ -302,6 +304,7 @@ begin
FTaskOptions := TJSArray(resultObj['taskOptions']);
FCategoryOptions := TJSArray(resultObj['categoryOptions']);
FPlaceOptions := TJSArray(resultObj['placeOptions']);
itemsArray := TJSArray(resultObj['items']);
xdwdsTimeEntries.Close;
......@@ -311,6 +314,7 @@ begin
FActiveRowIndex := -1;
HideRowValidationMessage;
SetTopControlsEnabled(True);
btnDeleteEntry.Enabled := False;
RenderTable;
finally
......@@ -322,6 +326,7 @@ end;
[async] function TFTimeEntries.AddTimeEntry: Boolean;
var
response: TXDataClientResponse;
resultObj: TJSObject;
begin
Result := False;
......@@ -337,15 +342,25 @@ begin
Exit;
end;
if (FActiveRowIndex >= 0) and (not ValidateRow(FActiveRowIndex)) then
begin
ApplyRowValidation(FActiveRowIndex);
ShowRowValidationMessage(FActiveRowIndex, 'Complete required fields before adding another entry.');
SetTopControlsEnabled(False);
Exit;
end;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.AddTimeEntry',
[FUserId, edtWeekOf.Text]
));
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;
except
on E: EXDataClientRequestException do
......@@ -407,7 +422,7 @@ var
'value="' + HtmlEncode(Value) + '">';
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
i: Integer;
optionObj: TJSObject;
......@@ -417,43 +432,60 @@ var
begin
triggerId := 'time_dd_' + FieldName + '_' + IntToStr(AIdx);
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" ' +
'type="button" data-bs-toggle="dropdown" aria-expanded="false" ' +
Result := '';
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" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '">' +
'<span class="time-dd-label text-truncate">' + HtmlEncode(CurrentDisplay) + '</span>' +
'<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>' +
'</button>' +
'<div class="dropdown-menu w-100 p-0 overflow-hidden">';
Result := Result +
'<button type="button" class="dropdown-item time-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="" ' +
'data-display="" ' +
'data-trigger-id="' + triggerId + '"></button>';
'<div class="dropdown-menu w-100 p-0 pe-1 overflow-hidden">';
if AAllowBlank then
begin
Result := Result +
'<button type="button" class="dropdown-item time-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="" ' +
'data-display="" ' +
'data-trigger-id="' + triggerId + '">&nbsp;</button>';
end;
for i := 0 to Items.length - 1 do
begin
optionObj := TJSObject(Items[i]);
optionValue := string(optionObj[ValueProp]);
optionDisplay := string(optionObj[DisplayProp]);
Result := Result +
'<button type="button" class="dropdown-item time-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="' + HtmlEncode(optionValue) + '" ' +
'data-display="' + HtmlEncode(optionDisplay) + '" ' +
'data-trigger-id="' + triggerId + '">' + HtmlEncode(optionDisplay) + '</button>';
end;
for i := 0 to Items.length - 1 do
begin
optionObj := TJSObject(Items[i]);
optionValue := string(optionObj[ValueProp]);
optionDisplay := string(optionObj[DisplayProp]);
Result := Result +
'<button type="button" class="dropdown-item time-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="' + HtmlEncode(optionValue) + '" ' +
'data-display="' + HtmlEncode(optionDisplay) + '" ' +
'data-trigger-id="' + triggerId + '">' + HtmlEncode(optionDisplay) + '</button>';
end;
Result := Result +
'</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;
function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string;
......@@ -470,12 +502,13 @@ begin
html :=
'<div class="time-vscroll">' +
'<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>' +
'<col style="width:140px">' + // Date
'<col style="width:500px">' + // Task
'<col style="width:90px">' + // Hours
'<col style="width:150px">' + // Time
'<col style="width:160px">' + // Place
'<col style="width:190px">' + // Category
'<col style="width:480px">' + // Summary
'</colgroup>' +
......@@ -484,6 +517,7 @@ begin
Th('Task') +
Th('Hours') +
Th('Time') +
Th('Place') +
Th('Category') +
Th('Summary') +
'</tr></thead><tbody>';
......@@ -497,21 +531,23 @@ begin
else
hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat);
html := html +
'<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(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay')) +
TdNowrap(HoursInput('hours', hoursText, rowIdx)) +
TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) +
TdNowrap(SelectList('category', xdwdsTimeEntriescategory.AsString, xdwdsTimeEntriescategoryDesc.AsString, rowIdx, FCategoryOptions, 'code', 'codeDesc')) +
TdWrap(SummaryTextArea('summary', xdwdsTimeEntriessummary.AsString, rowIdx)) +
'</tr>';
html := html +
'<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(SelectList('taskId', xdwdsTimeEntriestaskId.AsString, xdwdsTimeEntriestaskDisplay.AsString, rowIdx, FTaskOptions, 'taskId', 'taskDisplay', False, True)) +
TdNowrap(HoursInput('hours', hoursText, rowIdx)) +
TdNowrap(TextInput('taskTime', xdwdsTimeEntriestaskTime.AsString, rowIdx)) +
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)) +
'</tr>';
xdwdsTimeEntries.Next;
Inc(rowIdx);
end;
html := html + '</tbody></table></div></div>';
SetTotalRowsLabel(rowIdx);
host.innerHTML := html;
BindTableEditors;
......@@ -519,6 +555,7 @@ begin
EnableColumnResize;
RestoreTableScroll;
ApplyPendingEntryFocus;
ApplyActiveRowState;
end;
procedure TFTimeEntries.BindTableEditors;
......@@ -535,6 +572,7 @@ begin
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
end;
nodes := document.querySelectorAll('.time-dd-item');
......@@ -545,6 +583,14 @@ begin
el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
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');
console.log('BindTableEditors: time-row-selectable count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
......@@ -577,6 +623,7 @@ begin
Exit;
FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then
......@@ -616,65 +663,6 @@ begin
end;
[async] procedure TFTimeEntries.SaveRow(AIndex: Integer);
var
saveObj: TJSObject;
response: TXDataClientResponse;
rowEl: TJSHTMLElement;
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
if not xdwdsTimeEntries.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTimeEntries.Eof then
Exit;
if not ValidateRow(AIndex) then
begin
ApplyRowValidation(AIndex);
ShowRowValidationMessage(AIndex, 'Complete required fields before leaving this row.');
Exit;
end;
saveObj := TJSObject.new;
saveObj['entryId'] := xdwdsTimeEntriesentryId.AsString;
saveObj['userId'] := FUserId;
saveObj['taskDate'] := xdwdsTimeEntriestaskDate.AsString;
saveObj['taskId'] := xdwdsTimeEntriestaskId.AsString;
saveObj['hours'] := xdwdsTimeEntrieshours.AsFloat;
saveObj['taskTime'] := xdwdsTimeEntriestaskTime.AsString;
saveObj['category'] := xdwdsTimeEntriescategory.AsString;
saveObj['summary'] := xdwdsTimeEntriessummary.AsString;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.SaveTimeEntry',
[saveObj]
));
console.log('SaveRow response=' + string(TJSJSON.stringify(response.Result)));
rowEl := TJSHTMLElement(document.querySelector('tr[data-idx="' + IntToStr(AIndex) + '"]'));
nodes := rowEl.querySelectorAll('[data-unsaved-data="1"]');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.removeAttribute('data-unsaved-data');
end;
ClearRowValidation(AIndex);
except
on E: EXDataClientRequestException do
begin
console.log('SaveRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTimeEntries.RowClick(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
......@@ -686,7 +674,22 @@ begin
if idx < 0 then
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;
btnDeleteEntry.Enabled := True;
ApplyActiveRowState;
end;
......@@ -713,6 +716,7 @@ begin
if not ValidateRow(idx) then
begin
FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
ApplyRowValidation(idx);
ShowRowValidationMessage(idx, 'Complete required fields before leaving this row.');
SetTopControlsEnabled(False);
......@@ -721,9 +725,6 @@ begin
ClearRowValidation(idx);
SetTopControlsEnabled(True);
if Assigned(rowEl.querySelector('[data-unsaved-data="1"]')) then
SaveRow(idx);
end,
0
);
......@@ -770,6 +771,11 @@ begin
xdwdsTimeEntriestaskId.AsString := newValue;
xdwdsTimeEntriestaskDisplay.AsString := newDisplay;
end
else if SameText(fieldName, 'place') then
begin
xdwdsTimeEntriesplace.AsString := newValue;
xdwdsTimeEntriesplaceDesc.AsString := newDisplay;
end
else if SameText(fieldName, 'category') then
begin
xdwdsTimeEntriescategory.AsString := newValue;
......@@ -781,13 +787,19 @@ begin
xdwdsTimeEntries.Post;
FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
btn := TJSHTMLElement(document.getElementById(triggerId));
btn.setAttribute('data-unsaved-data', '1');
if Assigned(btn) then
begin
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label'));
if Assigned(labelEl) then
labelEl.textContent := newDisplay;
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label'));
labelEl.textContent := newDisplay;
btn.focus;
btn.focus;
end;
SaveField(idx, fieldName);
if ValidateRow(idx) then
begin
......@@ -853,11 +865,21 @@ begin
end;
FActiveRowIndex := idx;
btnDeleteEntry.Enabled := True;
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
firstEditor.focus;
begin
asm
firstEditor.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
firstEditor.focus();
end;
end;
FPendingEntryId := '';
end;
......@@ -975,10 +997,11 @@ begin
el := ATarget;
while Assigned(el) do
begin
idxText := string(el.getAttribute('data-idx'));
if idxText <> '' then
if el.hasAttribute('data-idx') then
begin
idxText := string(el.getAttribute('data-idx'));
Result := StrToIntDef(idxText, -1);
if Result >= 0 then
Exit;
end;
......@@ -989,8 +1012,32 @@ end;
procedure TFTimeEntries.DocumentMouseDown(Event: TJSEvent);
var
targetEl: TJSHTMLElement;
targetRowIndex: Integer;
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;
end;
......@@ -1055,7 +1102,7 @@ begin
if await(AddTimeEntry) then
begin
CaptureTableScroll;
LoadTimeEntries;
await(LoadTimeEntries);
end;
finally
Utils.HideSpinner('spinner');
......@@ -1063,6 +1110,52 @@ begin
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;
begin
asm
......@@ -1091,7 +1184,236 @@ begin
edtWeekOf.Enabled := AEnabled;
edtStartDate.Enabled := AEnabled;
edtEndDate.Enabled := AEnabled;
btnSearchRange.Enabled := AEnabled;
btnAddEntry.Enabled := AEnabled;
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.
......@@ -17,7 +17,8 @@ uses
uDropdownHelpers in 'uDropdownHelpers.pas',
View.Login in 'View.Login.pas' {FViewLogin: 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}
......
......@@ -156,6 +156,7 @@
<Form>FTimeEntries</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uTaskPickerOffCanvas.pas"/>
<None Include="index.html"/>
<None Include="css\app.css"/>
<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
AutoCommit = False
ProviderName = 'MySQL'
Database = 'eTask'
Username = 'root'
Server = '192.168.102.131'
Connected = True
LoginPrompt = False
Left = 435
Top = 359
......@@ -1149,8 +1152,14 @@ object ApiDatabase: TApiDatabase
'SET'
' TASK_DATE = :TASK_DATE,'
' TASK_ID = :TASK_ID,'
' PROJECT_ID = ('
' SELECT t.PROJECT_ID'
' FROM tasks t'
' WHERE t.TASK_ID = :TASK_ID'
' ),'
' HOURS = :HOURS,'
' TASK_TIME = :TASK_TIME,'
' PLACE = :PLACE,'
' CATEGORY = :CATEGORY,'
' SUMMARY = :SUMMARY,'
' MODIFY_DATE = now(),'
......@@ -1182,6 +1191,11 @@ object ApiDatabase: TApiDatabase
end
item
DataType = ftUnknown
Name = 'PLACE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY'
Value = nil
end
......@@ -1206,4 +1220,145 @@ object ApiDatabase: TApiDatabase
Value = nil
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
......@@ -90,6 +90,23 @@ type
uqProjectReportedUsersNAME: TStringField;
uqAddTimeEntry: 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 uqUsersCalcFields(DataSet: TDataSet);
private
......
......@@ -20,6 +20,8 @@ type
taskDisplay: string;
hours: Nullable<Double>;
taskTime: string;
place: string;
placeDesc: string;
category: string;
categoryDesc: string;
summary: string;
......@@ -44,6 +46,7 @@ type
items: TList<TTimeEntry>;
taskOptions: TList<TTimeEntryTaskOption>;
categoryOptions: TList<TTimeEntryCategoryOption>;
placeOptions: TList<TTimeEntryCategoryOption>;
constructor Create;
destructor Destroy; override;
end;
......@@ -56,16 +59,45 @@ type
taskId: string;
hours: Double;
taskTime: string;
place: string;
category: string;
summary: string;
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)]
ITimeEntryService = interface(IInvokable)
['{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}']
[HttpGet] function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
[HttpPost] function AddTimeEntry(userId, taskDate: string): string;
[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;
implementation
......@@ -76,6 +108,7 @@ begin
items := TList<TTimeEntry>.Create;
taskOptions := TList<TTimeEntryTaskOption>.Create;
categoryOptions := TList<TTimeEntryCategoryOption>.Create;
placeOptions := TList<TTimeEntryCategoryOption>.Create;
end;
destructor TTimeEntriesResponse.Destroy;
......@@ -83,6 +116,19 @@ begin
items.Free;
taskOptions.Free;
categoryOptions.Free;
placeOptions.Free;
inherited;
end;
constructor TTaskPickerOptionsResponse.Create;
begin
inherited;
options := TList<TTaskPickerOption>.Create;
end;
destructor TTaskPickerOptionsResponse.Destroy;
begin
options.Free;
inherited;
end;
......
......@@ -26,12 +26,18 @@ type
procedure LoadUserName(AResponse: TTimeEntriesResponse; userId: string);
procedure LoadTimeEntryItems(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
procedure LoadTaskOptions(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
procedure LoadPlaceOptions(AResponse: TTimeEntriesResponse);
procedure LoadCategoryOptions(AResponse: TTimeEntriesResponse);
function GetNextIdValue(const AKeyName: string): string;
public
function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
function AddTimeEntry(userId, taskDate: string): string;
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;
implementation
......@@ -101,6 +107,19 @@ begin
AddPart(FieldText(AQuery, 'TASK_NUM_6'));
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;
var
......@@ -187,6 +206,7 @@ begin
' ti.TASK_ID, ' +
' ti.HOURS, ' +
' ti.TASK_TIME, ' +
' ti.PLACE, ' +
' ti.CATEGORY, ' +
' cat.CODE_DESC as CATEGORY_DESC, ' +
' ti.SUMMARY, ' +
......@@ -198,12 +218,14 @@ begin
' t.TASK_NUM_4, ' +
' t.TASK_NUM_5, ' +
' t.TASK_NUM_6, ' +
' t.SUBJECT as TASK_SUBJECT ' +
' t.SUBJECT as TASK_SUBJECT, ' +
' plc.CODE_DESC as PLACE_DESC ' +
'from time_items ti ' +
'left join tasks t on t.TASK_ID = ti.TASK_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 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 ' +
' and ti.TASK_DATE >= :START_DATE ' +
' and ti.TASK_DATE < :END_DATE ' +
......@@ -234,6 +256,12 @@ begin
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.categoryDesc := FieldText(itemsQuery, 'CATEGORY_DESC');
......@@ -271,32 +299,15 @@ begin
' t.TASK_NUM_5, ' +
' t.TASK_NUM_6, ' +
' 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 customers c on c.CUSTOMER_ID = p.CUSTOMER_ID ' +
'where ( ' +
' ( ' +
' 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 ' +
' ) ' +
') ' +
'where tau.USER_ID = :USER_ID ' +
' and upper(trim(coalesce(tau.FOCUSED_TASK, ''''))) = ''T'' ' +
'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_EXISTING').AsString := userId;
tasksQuery.ParamByName('ACTIVE_DATE').AsDateTime := startDateValue;
tasksQuery.ParamByName('START_DATE').AsDateTime := startDateValue;
tasksQuery.ParamByName('END_DATE').AsDateTime := exclusiveEndDateValue;
tasksQuery.Open;
while not tasksQuery.Eof do
......@@ -350,6 +361,40 @@ begin
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;
var
startDateValue: TDateTime;
......@@ -368,6 +413,7 @@ begin
LoadUserName(Result, userId);
LoadTimeEntryItems(Result, userId, startDateValue, exclusiveEndDateValue);
LoadTaskOptions(Result, userId, startDateValue, exclusiveEndDateValue);
LoadPlaceOptions(Result);
LoadCategoryOptions(Result);
Result.count := Result.items.Count;
......@@ -433,13 +479,31 @@ end;
function TTimeEntryService.SaveTimeEntry(Item: TTimeEntrySave): Boolean;
var
taskDateValue: TDateTime;
entryId: string;
isNewEntry: Boolean;
begin
Logger.Log(4, Format('TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"', [Item.entryId, Item.userId]));
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.ParamByName('ENTRY_ID').AsString := Item.entryId;
apiDB.uqSaveTimeEntry.ParamByName('ENTRY_ID').AsString := entryId;
apiDB.uqSaveTimeEntry.ParamByName('USER_ID').AsString := Item.userId;
apiDB.uqSaveTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqSaveTimeEntry.ParamByName('TASK_ID').AsString := Item.taskId;
......@@ -450,6 +514,11 @@ begin
else
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('SUMMARY').AsString := Item.summary;
apiDB.uqSaveTimeEntry.ParamByName('MODIFIED_BY').AsString := Item.userId;
......@@ -458,7 +527,286 @@ begin
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;
......
......@@ -2,7 +2,7 @@
MemoLogLevel=4
FileLogLevel=4
webClientVersion=0.8.9
LogFileNum=222
LogFileNum=236
[Database]
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