Commit f6aacf29 by Mac Stephens

Add time items backend and client base functionality to load time entries and add new entry, WIP

parent 30fc30ba
...@@ -574,7 +574,7 @@ begin ...@@ -574,7 +574,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddTaskRow', [FTaskId, insertAfterItemNum] 'ITaskItemService.AddTaskRow', [FTaskId, insertAfterItemNum]
)); ));
console.log('AddTaskRow response=' + string(TJSJSON.stringify(response.Result))); console.log('AddTaskRow response=' + string(TJSJSON.stringify(response.Result)));
...@@ -661,13 +661,13 @@ var ...@@ -661,13 +661,13 @@ var
titleText: string; titleText: string;
rowCount: Integer; rowCount: Integer;
begin begin
console.log('IApiService.GetTaskItems called with task_id: ' + ATaskId); console.log('ITaskItemService.GetTaskItems called with task_id: ' + ATaskId);
console.log('Load Tasks Fired'); console.log('Load Tasks Fired');
Utils.ShowSpinner('spinner'); Utils.ShowSpinner('spinner');
try try
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.GetTaskItems', [ATaskId] 'ITaskItemService.GetTaskItems', [ATaskId]
)); ));
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
...@@ -1031,7 +1031,7 @@ begin ...@@ -1031,7 +1031,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow', 'ITaskItemService.MoveTaskRow',
[ [
StrToIntDef(xdwdsTaskstaskId.AsString, 0), StrToIntDef(xdwdsTaskstaskId.AsString, 0),
movedTaskItemId, movedTaskItemId,
...@@ -1106,7 +1106,7 @@ begin ...@@ -1106,7 +1106,7 @@ begin
Exit; Exit;
try try
response := await(xdwcTasks.RawInvokeAsync('IApiService.SaveTaskItemField', [payload])); response := await(xdwcTasks.RawInvokeAsync('ITaskItemService.SaveTaskItemField', [payload]));
console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result))); console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result)));
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
...@@ -1129,7 +1129,7 @@ begin ...@@ -1129,7 +1129,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteTaskRow', 'ITaskItemService.DeleteTaskRow',
[FSelectedTaskId, FSelectedTaskItemId] [FSelectedTaskId, FSelectedTaskItemId]
)); ));
console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result))); console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result)));
...@@ -1323,7 +1323,7 @@ begin ...@@ -1323,7 +1323,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddAssignedName', 'ITaskItemService.AddAssignedName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
...@@ -1349,7 +1349,7 @@ begin ...@@ -1349,7 +1349,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameAssignedName', 'ITaskItemService.RenameAssignedName',
[FTaskId, Trim(AOldName), Trim(ANewName)] [FTaskId, Trim(AOldName), Trim(ANewName)]
)); ));
...@@ -1375,7 +1375,7 @@ begin ...@@ -1375,7 +1375,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteAssignedName', 'ITaskItemService.DeleteAssignedName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
...@@ -1500,7 +1500,7 @@ begin ...@@ -1500,7 +1500,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddApplicationName', 'ITaskItemService.AddApplicationName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
...@@ -1526,7 +1526,7 @@ begin ...@@ -1526,7 +1526,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameApplicationName', 'ITaskItemService.RenameApplicationName',
[FTaskId, Trim(AOldName), Trim(ANewName)] [FTaskId, Trim(AOldName), Trim(ANewName)]
)); ));
...@@ -1552,7 +1552,7 @@ begin ...@@ -1552,7 +1552,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteApplicationName', 'ITaskItemService.DeleteApplicationName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
...@@ -1579,7 +1579,7 @@ begin ...@@ -1579,7 +1579,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddReportedName', 'ITaskItemService.AddReportedName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
...@@ -1605,7 +1605,7 @@ begin ...@@ -1605,7 +1605,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameReportedName', 'ITaskItemService.RenameReportedName',
[FTaskId, Trim(AOldName), Trim(ANewName)] [FTaskId, Trim(AOldName), Trim(ANewName)]
)); ));
...@@ -1631,7 +1631,7 @@ begin ...@@ -1631,7 +1631,7 @@ begin
try try
response := await(xdwcTasks.RawInvokeAsync( response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteReportedName', 'ITaskItemService.DeleteReportedName',
[FTaskId, Trim(AName)] [FTaskId, Trim(AName)]
)); ));
......
...@@ -2,4 +2,100 @@ object FTimeEntries: TFTimeEntries ...@@ -2,4 +2,100 @@ object FTimeEntries: TFTimeEntries
Width = 640 Width = 640
Height = 480 Height = 480
ElementFont = efCSS ElementFont = efCSS
OnCreate = WebFormCreate
object edtWeekOf: TWebEdit
Left = 71
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 1
ElementID = 'edt_week_of'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtWeekOf'
WidthPercent = 100.000000000000000000
OnChange = edtWeekOfChange
end
object edtStartDate: TWebEdit
Left = 245
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 2
ElementID = 'edt_start_date'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtStartDate'
WidthPercent = 100.000000000000000000
OnChange = edtStartDateChange
end
object edtEndDate: TWebEdit
Left = 415
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 3
ElementID = 'edt_end_date'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtEndDate'
WidthPercent = 100.000000000000000000
OnChange = edtEndDateChange
end
object btnAddEntry: TWebButton
Left = 440
Top = 24
Width = 96
Height = 25
Caption = 'Add Entry'
ChildOrder = 3
ElementID = 'btn_add_entry'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnAddEntryClick
end
object xdwcTimeEntries: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 460
Top = 156
end
object xdwdsTimeEntries: TXDataWebDataSet
Left = 322
Top = 156
object xdwdsTimeEntriesentryId: TIntegerField
FieldName = 'entryId'
end
object xdwdsTimeEntriestaskDate: TStringField
FieldName = 'taskDate'
end
object xdwdsTimeEntriestaskId: TStringField
FieldName = 'taskId'
end
object xdwdsTimeEntriestaskDisplay: TStringField
FieldName = 'taskDisplay'
end
object xdwdsTimeEntrieshours: TFloatField
FieldName = 'hours'
end
object xdwdsTimeEntriestaskTime: TStringField
FieldName = 'taskTime'
end
object xdwdsTimeEntriescategory: TStringField
FieldName = 'category'
end
object xdwdsTimeEntriescategoryDesc: TStringField
FieldName = 'categoryDesc'
end
object xdwdsTimeEntriessummary: TStringField
FieldName = 'summary'
end
end
end end
<html> <div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<head> <div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0 gap-3">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <h5 class="mb-0 text-nowrap" id="lbl_time_entries_title"></h5>
<title>TMS Web Project</title>
<style> <div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
</style> <div id="lbl_time_total_rows" class="me-2 text-nowrap"></div>
</head>
<body> <div class="d-flex align-items-center gap-2 flex-nowrap">
</body> <label for="edt_week_of" class="form-label mb-0 text-nowrap small">Week of</label>
</html> <input id="edt_week_of" type="date" class="form-control form-control-sm time-date-picker">
\ No newline at end of file </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">
</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">
</div>
<button id="btn_add_entry" class="btn btn-sm btn-success text-nowrap">Add Entry</button>
</div>
</div>
<div id="time_entries_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div>
</div>
...@@ -3,15 +3,59 @@ unit View.TimeEntries; ...@@ -3,15 +3,59 @@ unit View.TimeEntries;
interface interface
uses uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls, System.SysUtils, System.Classes, System.DateUtils, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs; WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.ExtCtrls,
XData.Web.Client, ConnectionModule, XData.Web.JsonDataset, Data.DB, XData.Web.Dataset;
type type
TFTimeEntries = class(TWebForm) TFTimeEntries = class(TWebForm)
edtWeekOf: TWebEdit;
edtStartDate: TWebEdit;
edtEndDate: TWebEdit;
xdwcTimeEntries: TXDataWebClient;
xdwdsTimeEntries: TXDataWebDataSet;
xdwdsTimeEntriesentryId: TIntegerField;
xdwdsTimeEntriestaskDate: TStringField;
xdwdsTimeEntriestaskId: TStringField;
xdwdsTimeEntriestaskDisplay: TStringField;
xdwdsTimeEntrieshours: TFloatField;
xdwdsTimeEntriestaskTime: TStringField;
xdwdsTimeEntriescategory: TStringField;
xdwdsTimeEntriessummary: TStringField;
xdwdsTimeEntriescategoryDesc: TStringField;
btnAddEntry: TWebButton;
procedure edtWeekOfChange(Sender: TObject);
procedure edtStartDateChange(Sender: TObject);
procedure edtEndDateChange(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
[async] procedure btnAddEntryClick(Sender: TObject);
private private
{ Private declarations } FUserId: string;
FUserName: string;
FStartDate: string;
FEndDate: string;
FTaskOptions: TJSArray;
FCategoryOptions: TJSArray;
FUpdatingDates: Boolean;
FPendingScrollTop: Integer;
FPendingScrollLeft: Integer;
[async] procedure LoadTimeEntries;
[async] function AddTimeEntry: Boolean;
procedure RenderTable;
procedure BindTableEditors;
procedure DropdownItemClick(Event: TJSEvent);
procedure EnableColumnResize;
procedure EnableAutoGrowTextAreas;
procedure GotoRowIndex(AIndex: Integer);
function HtmlEncode(const s: string): string;
function IsoToDate(const AValue: string): TDateTime;
function DateToIso(const AValue: TDateTime): string;
procedure SetWeekRangeFromAnchor(const ADateValue: string; ALoadAfterSet: Boolean);
procedure SetTotalRowsLabel(ARowCount: Integer);
procedure SetTimeEntriesLabel(const AName: string);
procedure CaptureTableScroll;
procedure RestoreTableScroll;
public public
{ Public declarations }
end; end;
var var
...@@ -19,6 +63,642 @@ var ...@@ -19,6 +63,642 @@ var
implementation implementation
uses
Auth.Service,
Utils;
{$R *.dfm} {$R *.dfm}
procedure TFTimeEntries.WebFormCreate(Sender: TObject);
var
payload: TJSObject;
anchorDate: string;
begin
console.log('TFTimeEntries.WebFormCreate fired');
FTaskOptions := TJSArray.new;
FCategoryOptions := TJSArray.new;
FUpdatingDates := False;
FPendingScrollTop := 0;
FPendingScrollLeft := 0;
payload := AuthService.TokenPayload;
if Assigned(payload) then
begin
FUserId := JS.toString(payload.Properties['user_id']);
FUserName := JS.toString(payload.Properties['user_fullname']);
if FUserName = '' then
FUserName := JS.toString(payload.Properties['user_name']);
end;
SetTimeEntriesLabel(FUserName);
anchorDate := Application.Parameters.Values['date'];
if anchorDate = '' then
anchorDate := DateToIso(Date);
SetWeekRangeFromAnchor(anchorDate, False);
if not DMConnection.ApiConnection.Connected then
begin
DMConnection.ApiConnection.Open(
procedure
begin
LoadTimeEntries;
end
);
end
else
LoadTimeEntries;
end;
function TFTimeEntries.HtmlEncode(const s: string): string;
begin
Result := s;
Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;
function TFTimeEntries.IsoToDate(const AValue: string): TDateTime;
var
yearValue: Integer;
monthValue: Integer;
dayValue: Integer;
begin
if Length(AValue) < 10 then
Exit(Date);
yearValue := StrToIntDef(Copy(AValue, 1, 4), YearOf(Date));
monthValue := StrToIntDef(Copy(AValue, 6, 2), MonthOf(Date));
dayValue := StrToIntDef(Copy(AValue, 9, 2), DayOf(Date));
Result := EncodeDate(yearValue, monthValue, dayValue);
end;
function TFTimeEntries.DateToIso(const AValue: TDateTime): string;
begin
Result := FormatDateTime('yyyy-mm-dd', AValue);
end;
procedure TFTimeEntries.SetWeekRangeFromAnchor(const ADateValue: string; ALoadAfterSet: Boolean);
var
anchorDate: TDateTime;
weekStart: TDateTime;
weekEnd: TDateTime;
begin
anchorDate := IsoToDate(ADateValue);
weekStart := anchorDate - (DayOfTheWeek(anchorDate) - 1);
weekEnd := weekStart + 6;
FUpdatingDates := True;
try
edtWeekOf.Text := DateToIso(anchorDate);
edtStartDate.Text := DateToIso(weekStart);
edtEndDate.Text := DateToIso(weekEnd);
FStartDate := edtStartDate.Text;
FEndDate := edtEndDate.Text;
finally
FUpdatingDates := False;
end;
if ALoadAfterSet then
LoadTimeEntries;
end;
procedure TFTimeEntries.edtWeekOfChange(Sender: TObject);
begin
if FUpdatingDates then
Exit;
SetWeekRangeFromAnchor(edtWeekOf.Text, True);
end;
procedure TFTimeEntries.edtStartDateChange(Sender: TObject);
begin
if FUpdatingDates then
Exit;
FStartDate := edtStartDate.Text;
LoadTimeEntries;
end;
procedure TFTimeEntries.edtEndDateChange(Sender: TObject);
begin
if FUpdatingDates then
Exit;
FEndDate := edtEndDate.Text;
LoadTimeEntries;
end;
procedure TFTimeEntries.GotoRowIndex(AIndex: Integer);
var
i: Integer;
begin
if (AIndex < 0) or (not xdwdsTimeEntries.Active) then
Exit;
xdwdsTimeEntries.First;
i := 0;
while (i < AIndex) and (not xdwdsTimeEntries.Eof) do
begin
xdwdsTimeEntries.Next;
Inc(i);
end;
end;
procedure TFTimeEntries.SetTotalRowsLabel(ARowCount: Integer);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_time_total_rows'));
if Assigned(el) then
el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
end;
procedure TFTimeEntries.SetTimeEntriesLabel(const AName: string);
var
el: TJSHTMLElement;
displayName: string;
begin
displayName := Trim(AName);
if displayName = '' then
displayName := 'User';
el := TJSHTMLElement(document.getElementById('lbl_time_entries_title'));
if Assigned(el) then
el.innerText := displayName + '''s Time Entries';
end;
[async] procedure TFTimeEntries.LoadTimeEntries;
var
response: TXDataClientResponse;
resultObj: TJSObject;
itemsArray: TJSArray;
userNameValue: string;
rowCount: Integer;
begin
console.log('ITimeEntryService.GetTimeEntries called with user_id=' + FUserId + ' start=' + FStartDate + ' end=' + FEndDate);
if FUserId = '' then
begin
Utils.ShowErrorModal('Unable to determine logged-in user.');
Exit;
end;
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.GetTimeEntries', [FUserId, FStartDate, FEndDate]
));
except
on E: EXDataClientRequestException do
begin
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
Exit;
end;
end;
if not Assigned(response.Result) then
Exit;
resultObj := TJSObject(response.Result);
if resultObj.hasOwnProperty('userName') then
begin
userNameValue := string(resultObj['userName']);
if userNameValue <> '' then
SetTimeEntriesLabel(userNameValue);
end
else
SetTimeEntriesLabel(FUserName);
rowCount := StrToIntDef(string(resultObj['count']), 0);
SetTotalRowsLabel(rowCount);
if resultObj.hasOwnProperty('taskOptions') then
FTaskOptions := TJSArray(resultObj['taskOptions'])
else
FTaskOptions := TJSArray.new;
if resultObj.hasOwnProperty('categoryOptions') then
FCategoryOptions := TJSArray(resultObj['categoryOptions'])
else
FCategoryOptions := TJSArray.new;
itemsArray := TJSArray(resultObj['items']);
if not Assigned(itemsArray) then
itemsArray := TJSArray.new;
xdwdsTimeEntries.Close;
xdwdsTimeEntries.SetJsonData(itemsArray);
xdwdsTimeEntries.Open;
RenderTable;
finally
Utils.HideSpinner('spinner');
end;
end;
[async] function TFTimeEntries.AddTimeEntry: Boolean;
var
response: TXDataClientResponse;
begin
Result := False;
if FUserId = '' then
begin
Utils.ShowErrorModal('Unable to determine logged-in user.');
Exit;
end;
if edtWeekOf.Text = '' then
begin
Utils.ShowErrorModal('Select a week/date before adding an entry.');
Exit;
end;
try
response := await(xdwcTimeEntries.RawInvokeAsync(
'ITimeEntryService.AddTimeEntry',
[FUserId, edtWeekOf.Text]
));
console.log('AddTimeEntry response=' + string(TJSJSON.stringify(response.Result)));
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('AddTimeEntry ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTimeEntries.RenderTable;
var
host: TJSHTMLElement;
html: string;
rowIdx: Integer;
hoursText: string;
function Th(const s: string): string;
begin
Result := '<th scope="col">' + s + '</th>';
end;
function TdNowrap(const s: string): string;
begin
Result := '<td class="align-top nowrap-cell">' + s + '</td>';
end;
function TdWrap(const s: string): string;
begin
Result := '<td class="align-top wrap-cell">' + s + '</td>';
end;
function TextInput(const FieldName, Value: string; const AIdx: Integer): string;
begin
Result :=
'<input class="form-control form-control-sm cell-input time-editor w-100" readonly ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(Value) + '">';
end;
function DateInput(const FieldName, Value: string; const AIdx: Integer): string;
var
dateValue: string;
begin
dateValue := Utils.NormalizeDateValue(Value);
Result :=
'<input type="date" class="form-control form-control-sm cell-input time-editor w-100" readonly ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(dateValue) + '">';
end;
function HoursInput(const FieldName, Value: string; const AIdx: Integer): string;
begin
Result :=
'<input type="number" step="0.01" min="0" class="form-control form-control-sm cell-input time-editor w-100 text-end" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(Value) + '">';
end;
function SelectList(const FieldName, CurrentValue, CurrentDisplay: string; const AIdx: Integer; const Items: TJSArray; const ValueProp, DisplayProp: string): string;
var
i: Integer;
optionObj: TJSObject;
optionValue: string;
optionDisplay: string;
triggerId: string;
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" ' +
'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>';
if Assigned(Items) then
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>';
end;
function SummaryTextArea(const FieldName, Value: string; const AIdx: Integer): string;
begin
Result :=
'<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100" readonly ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'style="min-height:31px; height:31px; overflow:hidden; resize:none;">' + HtmlEncode(Value) + '</textarea>';
end;
begin
host := TJSHTMLElement(document.getElementById('time_entries_table_host'));
if not Assigned(host) then
Exit;
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;">' +
'<colgroup>' +
'<col style="width:140px">' + // Date
'<col style="width:500px">' + // Task
'<col style="width:90px">' + // Hours
'<col style="width:150px">' + // Time
'<col style="width:190px">' + // Category
'<col style="width:480px">' + // Summary
'</colgroup>' +
'<thead><tr>' +
Th('Date') +
Th('Task') +
Th('Hours') +
Th('Time') +
Th('Category') +
Th('Summary') +
'</tr></thead><tbody>';
rowIdx := 0;
xdwdsTimeEntries.First;
while not xdwdsTimeEntries.Eof do
begin
if xdwdsTimeEntrieshours.IsNull then
hoursText := ''
else
hoursText := FormatFloat('0.##', xdwdsTimeEntrieshours.AsFloat);
html := html +
'<tr 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>';
xdwdsTimeEntries.Next;
Inc(rowIdx);
end;
html := html + '</tbody></table></div></div>';
host.innerHTML := html;
BindTableEditors;
EnableAutoGrowTextAreas;
EnableColumnResize;
RestoreTableScroll;
end;
procedure TFTimeEntries.BindTableEditors;
var
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
console.log('BindTableEditors: wiring time entry handlers...');
nodes := document.querySelectorAll('.time-editor');
console.log('BindTableEditors: time-editor count=' + IntToStr(nodes.length));
nodes := document.querySelectorAll('.time-dd-item');
console.log('BindTableEditors: time-dd-item count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
end;
end;
procedure TFTimeEntries.DropdownItemClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr: string;
fieldName: string;
newValue: string;
newDisplay: string;
triggerId: string;
btn: TJSHTMLElement;
labelEl: TJSHTMLElement;
begin
if not xdwdsTimeEntries.Active then
Exit;
Event.preventDefault;
el := TJSHTMLElement(Event.currentTarget);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
newValue := string(el.getAttribute('data-value'));
newDisplay := string(el.getAttribute('data-display'));
triggerId := string(el.getAttribute('data-trigger-id'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
GotoRowIndex(idx);
if xdwdsTimeEntries.Eof then
Exit;
xdwdsTimeEntries.Edit;
if SameText(fieldName, 'taskId') then
begin
xdwdsTimeEntriestaskId.AsString := newValue;
xdwdsTimeEntriestaskDisplay.AsString := newDisplay;
end
else if SameText(fieldName, 'category') then
begin
xdwdsTimeEntriescategory.AsString := newValue;
xdwdsTimeEntriescategoryDesc.AsString := newDisplay;
end
else
xdwdsTimeEntries.FieldByName(fieldName).AsString := newValue;
xdwdsTimeEntries.Post;
if triggerId <> '' then
begin
btn := TJSHTMLElement(document.getElementById(triggerId));
if Assigned(btn) then
begin
labelEl := TJSHTMLElement(btn.querySelector('.time-dd-label'));
if Assigned(labelEl) then
labelEl.textContent := newDisplay;
end;
end;
end;
procedure TFTimeEntries.EnableAutoGrowTextAreas;
begin
asm
(function(){
const host = document.getElementById('time_entries_table_host');
if(!host) return;
host.querySelectorAll('.time-textarea').forEach(function(editor){
const fit = function() {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
};
fit();
editor.addEventListener('input', fit);
});
})();
end;
end;
procedure TFTimeEntries.EnableColumnResize;
begin
asm
(function(){
const host = document.getElementById('time_entries_table_host');
if(!host) return;
const table = host.querySelector('table');
if(!table) return;
const ths = table.querySelectorAll('thead th');
const cols = table.querySelectorAll('colgroup col');
ths.forEach((th, index) => {
th.classList.add('th-resize');
if (th.querySelector('.th-resize-handle')) return;
const handle = document.createElement('div');
handle.className = 'th-resize-handle';
th.appendChild(handle);
handle.addEventListener('mousedown', function(e){
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startW = th.getBoundingClientRect().width;
function onMove(ev){
const w = Math.max(20, startW + (ev.clientX - startX));
th.style.width = w + 'px';
th.style.minWidth = '0';
if (cols && cols[index]) {
cols[index].style.width = w + 'px';
}
}
function onUp(){
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
})();
end;
end;
[async] procedure TFTimeEntries.btnAddEntryClick(Sender: TObject);
begin
Utils.ShowSpinner('spinner');
try
if await(AddTimeEntry) then
begin
CaptureTableScroll;
LoadTimeEntries;
end;
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTimeEntries.CaptureTableScroll;
begin
asm
const vscroll = document.querySelector('.time-vscroll');
const hscroll = document.querySelector('.time-hscroll');
this.FPendingScrollTop = vscroll ? vscroll.scrollTop : 0;
this.FPendingScrollLeft = hscroll ? hscroll.scrollLeft : 0;
end;
end;
procedure TFTimeEntries.RestoreTableScroll;
begin
asm
const vscroll = document.querySelector('.time-vscroll');
const hscroll = document.querySelector('.time-hscroll');
if (vscroll) vscroll.scrollTop = this.FPendingScrollTop || 0;
if (hscroll) hscroll.scrollLeft = this.FPendingScrollLeft || 0;
end;
end;
end. end.
is-invalid .form-check-input { .is-invalid .form-check-input {
border: 1px solid #dc3545 !important; border: 1px solid #dc3545 !important;
} }
...@@ -22,6 +22,7 @@ is-invalid .form-check-input { ...@@ -22,6 +22,7 @@ is-invalid .form-check-input {
transform: translateX(-120%); transform: translateX(-120%);
opacity: 0; opacity: 0;
} }
to { to {
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
...@@ -39,37 +40,6 @@ is-invalid .form-check-input { ...@@ -39,37 +40,6 @@ is-invalid .form-check-input {
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.tasks-vscroll {
height: 100%;
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
span.card { span.card {
border: none; border: none;
...@@ -89,214 +59,6 @@ span.card { ...@@ -89,214 +59,6 @@ span.card {
user-select: none; user-select: none;
} }
.tasks-table {
table-layout: fixed;
}
.tasks-table th {
overflow: hidden;
}
.tasks-table td {
overflow: visible;
}
.nowrap-cell,
.wrap-cell {
overflow: visible;
}
.tasks-table .dropdown,
.task-dd-toggle,
.task-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.dropdown-menu { .dropdown-menu {
z-index: 1055; z-index: 1055;
} }
.task-toolbar {
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
background: var(--bs-body-bg);
white-space: nowrap;
}
.task-toolbar-btn {
min-width: 2rem;
padding: 0.25rem 0.5rem;
}
#lbl_total_rows {
white-space: nowrap;
}
.task-rich-editor {
white-space: pre-wrap;
overflow-wrap: anywhere;
height: auto;
}
.task-rich-editor:focus {
outline: 0;
}
.task-color-btn {
position: relative;
min-width: 2rem;
height: 2rem;
padding: 0.2rem 0.45rem;
}
.task-hidden-color-input {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
width: 1px;
height: 1px;
border: 0;
}
.task-color-glyph {
display: inline-block;
font-weight: 600;
line-height: 1;
}
.task-color-swatch {
position: absolute;
left: 0.35rem;
right: 0.35rem;
bottom: 0.2rem;
height: 0.18rem;
border-radius: 999px;
background: #000000;
}
.task-dd-toggle.status-cannot-duplicate {
--bs-btn-color: #41464b;
--bs-btn-bg: #e2e3e5;
--bs-btn-border-color: #c4c8cb;
--bs-btn-hover-color: #41464b;
--bs-btn-hover-bg: #d3d4d5;
--bs-btn-hover-border-color: #b9bcc0;
--bs-btn-active-color: #41464b;
--bs-btn-active-bg: #c6c7c8;
--bs-btn-active-border-color: #b9bcc0;
}
.task-dd-toggle.status-cannot-test {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-future-enhancement {
--bs-btn-color: #055160;
--bs-btn-bg: #cff4fc;
--bs-btn-border-color: #9eeaf9;
--bs-btn-hover-color: #055160;
--bs-btn-hover-bg: #b6effb;
--bs-btn-hover-border-color: #86e5f8;
--bs-btn-active-color: #055160;
--bs-btn-active-bg: #9eeaf9;
--bs-btn-active-border-color: #74dff6;
}
.task-dd-toggle.status-fixed-verified {
--bs-btn-color: #0f5132;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #0f5132;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #0f5132;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-fixed {
--bs-btn-color: #146c43;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #146c43;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #146c43;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-investigating {
--bs-btn-color: #664d03;
--bs-btn-bg: #fff3cd;
--bs-btn-border-color: #ffe69c;
--bs-btn-hover-color: #664d03;
--bs-btn-hover-bg: #ffecb5;
--bs-btn-hover-border-color: #ffdf7e;
--bs-btn-active-color: #664d03;
--bs-btn-active-bg: #ffe69c;
--bs-btn-active-border-color: #ffd966;
}
.task-dd-toggle.status-not-fixed {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-non-issue {
--bs-btn-color: #432874;
--bs-btn-bg: #e2d9f3;
--bs-btn-border-color: #cbbbe8;
--bs-btn-hover-color: #432874;
--bs-btn-hover-bg: #d6caee;
--bs-btn-hover-border-color: #bea9e2;
--bs-btn-active-color: #432874;
--bs-btn-active-bg: #cbbbe8;
--bs-btn-active-border-color: #b89ddd;
}
.task-dd-toggle.status-possibly-a-problem {
--bs-btn-color: #7a3e00;
--bs-btn-bg: #ffe5d0;
--bs-btn-border-color: #f7c79d;
--bs-btn-hover-color: #7a3e00;
--bs-btn-hover-bg: #ffd7b8;
--bs-btn-hover-border-color: #f2ba88;
--bs-btn-active-color: #7a3e00;
--bs-btn-active-bg: #f7c79d;
--bs-btn-active-border-color: #eeaf69;
}
.task-dd-toggle.status-default {
--bs-btn-color: #212529;
--bs-btn-bg: #f8f9fa;
--bs-btn-border-color: #dee2e6;
--bs-btn-hover-color: #212529;
--bs-btn-hover-bg: #e9ecef;
--bs-btn-hover-border-color: #ced4da;
--bs-btn-active-color: #212529;
--bs-btn-active-bg: #dee2e6;
--bs-btn-active-border-color: #ced4da;
}
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
.is-invalid .form-check-label {
color: #dc3545 !important;
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
@keyframes slideInLeft {
from {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.slide-in {
animation: slideInLeft 0.4s ease-out forwards;
}
#spinner {
position: fixed !important;
z-index: 9999 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.tasks-vscroll {
height: 100%;
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
span.card {
border: none;
}
.th-resize {
position: relative;
}
.th-resize-handle {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.tasks-table {
table-layout: fixed;
}
.tasks-table th {
overflow: hidden;
}
.tasks-table td {
overflow: visible;
}
.nowrap-cell,
.wrap-cell {
overflow: visible;
}
.tasks-table .dropdown,
.task-dd-toggle,
.task-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.dropdown-menu {
z-index: 1055;
}
.task-toolbar {
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
background: var(--bs-body-bg);
white-space: nowrap;
}
.task-toolbar-btn {
min-width: 2rem;
padding: 0.25rem 0.5rem;
}
#lbl_total_rows {
white-space: nowrap;
}
.task-rich-editor {
white-space: pre-wrap;
overflow-wrap: anywhere;
height: auto;
}
.task-rich-editor:focus {
outline: 0;
}
.task-color-btn {
position: relative;
min-width: 2rem;
height: 2rem;
padding: 0.2rem 0.45rem;
}
.task-hidden-color-input {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
width: 1px;
height: 1px;
border: 0;
}
.task-color-glyph {
display: inline-block;
font-weight: 600;
line-height: 1;
}
.task-color-swatch {
position: absolute;
left: 0.35rem;
right: 0.35rem;
bottom: 0.2rem;
height: 0.18rem;
border-radius: 999px;
background: #000000;
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th,
.time-table td {
overflow: hidden;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.task-dd-toggle.status-cannot-duplicate {
--bs-btn-color: #41464b;
--bs-btn-bg: #e2e3e5;
--bs-btn-border-color: #c4c8cb;
--bs-btn-hover-color: #41464b;
--bs-btn-hover-bg: #d3d4d5;
--bs-btn-hover-border-color: #b9bcc0;
--bs-btn-active-color: #41464b;
--bs-btn-active-bg: #c6c7c8;
--bs-btn-active-border-color: #b9bcc0;
}
.task-dd-toggle.status-cannot-test {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-future-enhancement {
--bs-btn-color: #055160;
--bs-btn-bg: #cff4fc;
--bs-btn-border-color: #9eeaf9;
--bs-btn-hover-color: #055160;
--bs-btn-hover-bg: #b6effb;
--bs-btn-hover-border-color: #86e5f8;
--bs-btn-active-color: #055160;
--bs-btn-active-bg: #9eeaf9;
--bs-btn-active-border-color: #74dff6;
}
.task-dd-toggle.status-fixed-verified {
--bs-btn-color: #0f5132;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #0f5132;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #0f5132;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-fixed {
--bs-btn-color: #146c43;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #146c43;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #146c43;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-investigating {
--bs-btn-color: #664d03;
--bs-btn-bg: #fff3cd;
--bs-btn-border-color: #ffe69c;
--bs-btn-hover-color: #664d03;
--bs-btn-hover-bg: #ffecb5;
--bs-btn-hover-border-color: #ffdf7e;
--bs-btn-active-color: #664d03;
--bs-btn-active-bg: #ffe69c;
--bs-btn-active-border-color: #ffd966;
}
.task-dd-toggle.status-not-fixed {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-non-issue {
--bs-btn-color: #432874;
--bs-btn-bg: #e2d9f3;
--bs-btn-border-color: #cbbbe8;
--bs-btn-hover-color: #432874;
--bs-btn-hover-bg: #d6caee;
--bs-btn-hover-border-color: #bea9e2;
--bs-btn-active-color: #432874;
--bs-btn-active-bg: #cbbbe8;
--bs-btn-active-border-color: #b89ddd;
}
.task-dd-toggle.status-possibly-a-problem {
--bs-btn-color: #7a3e00;
--bs-btn-bg: #ffe5d0;
--bs-btn-border-color: #f7c79d;
--bs-btn-hover-color: #7a3e00;
--bs-btn-hover-bg: #ffd7b8;
--bs-btn-hover-border-color: #f2ba88;
--bs-btn-active-color: #7a3e00;
--bs-btn-active-bg: #f7c79d;
--bs-btn-active-border-color: #eeaf69;
}
.task-dd-toggle.status-default {
--bs-btn-color: #212529;
--bs-btn-bg: #f8f9fa;
--bs-btn-border-color: #dee2e6;
--bs-btn-hover-color: #212529;
--bs-btn-hover-bg: #e9ecef;
--bs-btn-hover-border-color: #ced4da;
--bs-btn-active-color: #212529;
--bs-btn-active-bg: #dee2e6;
--bs-btn-active-border-color: #ced4da;
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th {
overflow: hidden;
}
.time-table td {
overflow: visible;
}
.time-table .nowrap-cell,
.time-table .wrap-cell {
overflow: visible;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.time-table .dropdown-menu {
z-index: 1055;
}
\ No newline at end of file
...@@ -84,6 +84,12 @@ begin ...@@ -84,6 +84,12 @@ begin
if SameText(timeEntriesParam, 'true') then if SameText(timeEntriesParam, 'true') then
begin begin
if AuthService.Authenticated and not AuthService.TokenExpired then
begin
DisplayMainView;
Exit;
end;
DisplayLoginView('', '', ''); DisplayLoginView('', '', '');
Exit; Exit;
end; end;
......
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>8</VerInfo_MinorVer>
<VerInfo_Release>9</VerInfo_Release> <VerInfo_Release>9</VerInfo_Release>
<TMSURLParams>?user_id=1019&amp;task_id=4045&amp;url_code=123456</TMSURLParams> <TMSURLParams>?time_entries=true&amp;date=2026-05-01</TMSURLParams>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSUseJSDebugger>2</TMSUseJSDebugger>
...@@ -160,6 +160,8 @@ ...@@ -160,6 +160,8 @@
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="config\config.json"/> <None Include="config\config.json"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
<None Include="css\task-items.css"/>
<None Include="css\time-entries.css"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
...@@ -240,6 +242,18 @@ ...@@ -240,6 +242,18 @@
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
<DeployFile LocalName="css\task-items.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\time-entries.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"/> <DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"> <DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32"> <Platform Name="Win32">
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/> <link href="css/app.css" rel="stylesheet"/>
<link href="css/task-items.css" rel="stylesheet"/>
<link href="css/time-entries.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet"/> <link href="css/spinner.css" rel="stylesheet"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script> <script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
...@@ -17,7 +19,7 @@ ...@@ -17,7 +19,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="$(ProjectName).js"></script> <script src="$(ProjectName).js"></script>
</head> <link href="css/task-items.css" rel="stylesheet"/><link href="css/time-entries.css" rel="stylesheet"/></head>
<body> <body>
<noscript>Your browser does not support JavaScript!</noscript> <noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script> <script>rtl.run();</script>
...@@ -27,3 +29,5 @@ ...@@ -27,3 +29,5 @@
object ApiDatabase: TApiDatabase object ApiDatabase: TApiDatabase
OnCreate = DataModuleCreate OnCreate = DataModuleCreate
Height = 475 Height = 632
Width = 996 Width = 994
object ucETaskApi: TUniConnection object ucETaskApi: TUniConnection
AutoCommit = False AutoCommit = False
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
LoginPrompt = False LoginPrompt = False
Left = 267 Left = 435
Top = 395 Top = 359
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF' EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end end
object MySQLUniProvider1: TMySQLUniProvider object MySQLUniProvider1: TMySQLUniProvider
Left = 416 Left = 546
Top = 398 Top = 356
end end
object uqUsers: TUniQuery object uqUsers: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
...@@ -142,7 +142,7 @@ object ApiDatabase: TApiDatabase ...@@ -142,7 +142,7 @@ object ApiDatabase: TApiDatabase
'from task_items' 'from task_items'
'where TASK_ID = :TASK_ID' 'where TASK_ID = :TASK_ID'
'order by ITEM_NUM') 'order by ITEM_NUM')
Left = 56 Left = 58
Top = 26 Top = 26
ParamData = < ParamData = <
item item
...@@ -282,7 +282,7 @@ object ApiDatabase: TApiDatabase ...@@ -282,7 +282,7 @@ object ApiDatabase: TApiDatabase
'left join project p' 'left join project p'
' on p.PROJECT_ID = t.PROJECT_ID' ' on p.PROJECT_ID = t.PROJECT_ID'
'where t.TASK_ID = :TASK_ID') 'where t.TASK_ID = :TASK_ID')
Left = 54 Left = 60
Top = 82 Top = 82
ParamData = < ParamData = <
item item
...@@ -367,7 +367,7 @@ object ApiDatabase: TApiDatabase ...@@ -367,7 +367,7 @@ object ApiDatabase: TApiDatabase
'from task_items' 'from task_items'
'where TASK_ITEM_ID = :TASK_ITEM_ID' 'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID') ' and TASK_ID = :TASK_ID')
Left = 240 Left = 236
Top = 16 Top = 16
ParamData = < ParamData = <
item item
...@@ -1085,4 +1085,61 @@ object ApiDatabase: TApiDatabase ...@@ -1085,4 +1085,61 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqAddTimeEntry: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert into time_items ('
' ENTRY_ID,'
' USER_ID,'
' TASK_DATE,'
' HOURS,'
' TASK_TIME,'
' CATEGORY,'
' SUMMARY,'
' CREATE_DATE,'
' CREATED_BY,'
' MODIFY_DATE,'
' MODIFIED_BY'
') values ('
' :ENTRY_ID,'
' :USER_ID,'
' :TASK_DATE,'
' null,'
' null,'
' null,'
' null,'
' now(),'
' :CREATED_BY,'
' now(),'
' :MODIFIED_BY'
')')
Left = 436
Top = 426
ParamData = <
item
DataType = ftUnknown
Name = 'ENTRY_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_DATE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CREATED_BY'
Value = nil
end
item
DataType = ftUnknown
Name = 'MODIFIED_BY'
Value = nil
end>
end
end end
...@@ -88,6 +88,7 @@ type ...@@ -88,6 +88,7 @@ type
uqRenameTaskItemReportedBy: TUniQuery; uqRenameTaskItemReportedBy: TUniQuery;
uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField; uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField;
uqProjectReportedUsersNAME: TStringField; uqProjectReportedUsersNAME: TStringField;
uqAddTimeEntry: TUniQuery;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -53,7 +53,7 @@ uses ...@@ -53,7 +53,7 @@ uses
XData.Sys.Exceptions, XData.Sys.Exceptions,
Common.Logging, Common.Logging,
Common.Middleware.Logging, Common.Middleware.Logging,
Common.Config, Vcl.Forms, IniFiles, Api.Service; Common.Config, Vcl.Forms, IniFiles, TaskItem.Service, TimeEntry.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'} {%CLASSGROUP 'Vcl.Controls.TControl'}
......
...@@ -299,7 +299,7 @@ begin ...@@ -299,7 +299,7 @@ begin
try try
jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36)); jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
jwt.Claims.IssuedAt := Now; jwt.Claims.IssuedAt := Now;
jwt.Claims.Expiration := IncHour(Now, 12); jwt.Claims.Expiration := IncHour(Now, 24);
jwt.Claims.SetClaimOfType<string>('user_id', Self.userId); jwt.Claims.SetClaimOfType<string>('user_id', Self.userId);
jwt.Claims.SetClaimOfType<string>('user_name', Self.userName); jwt.Claims.SetClaimOfType<string>('user_name', Self.userName);
......
unit Main; unit Main;
//Authors:
//Elias Sarraf
//Mac Stephens
//Cameron Hayes
interface interface
uses uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi, Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles, Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles,
Auth.Service, Auth.Server.Module, Api.Service, Api.Server.Module, App.Server.Module, TaskItem.Service, Auth.Server.Module, Auth.Service, Api.Server.Module, App.Server.Module,
ExeInfo; TimeEntry.Service, TimeEntry.ServiceImpl, ExeInfo;
type type
TFMain = class(TForm) TFMain = class(TForm)
......
unit Api.Service; unit TaskItem.Service;
interface interface
...@@ -115,10 +115,10 @@ type ...@@ -115,10 +115,10 @@ type
type type
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable) ITaskItemService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}'] ['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
function GetTaskItems(taskId: string): TTaskItemsResponse; [HttpGet] function GetTaskItems(taskId: string): TTaskItemsResponse;
[HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean; [HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
[HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; [HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
...@@ -134,9 +134,10 @@ type ...@@ -134,9 +134,10 @@ type
[HttpPost] function RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse; [HttpPost] function RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
[HttpPost] function DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse; [HttpPost] function DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer); [HttpPost] procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean; [HttpPost] function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean; [HttpPost] function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
end; end;
implementation implementation
...@@ -188,6 +189,6 @@ begin ...@@ -188,6 +189,6 @@ begin
end; end;
initialization initialization
RegisterServiceType(TypeInfo(IApiService)); RegisterServiceType(TypeInfo(ITaskItemService));
end. end.
unit Api.ServiceImpl; unit TaskItem.ServiceImpl;
interface interface
uses uses
XData.Server.Module, XData.Service.Common, XData.Server.Module, XData.Service.Common,
System.Variants, System.DateUtils, Uni, System.Variants, System.DateUtils, Uni,
Api.Service, Api.Database, Common.Logging, TaskItem.Service, Api.Database, Common.Logging,
System.SysUtils, JSON, System.IniFiles, DBAccess; System.SysUtils, JSON, System.IniFiles, DBAccess;
type type
[ServiceImplementation] [ServiceImplementation]
TApiService = class(TInterfacedObject, IApiService) TTaskItemService = class(TInterfacedObject, ITaskItemService)
strict private strict private
apiDB: TApiDatabase; apiDB: TApiDatabase;
private private
...@@ -47,22 +47,23 @@ type ...@@ -47,22 +47,23 @@ type
implementation implementation
procedure TApiService.AfterConstruction; procedure TTaskItemService.AfterConstruction;
begin begin
inherited; inherited;
apiDB := TApiDatabase.Create(nil); apiDB := TApiDatabase.Create(nil);
Logger.Log(4, 'ApiService.AfterConstruction - apiDB created'); Logger.Log(4, 'TaskItemService.AfterConstruction - apiDB created');
end; end;
procedure TApiService.BeforeDestruction;
procedure TTaskItemService.BeforeDestruction;
begin begin
Logger.Log(4, 'ApiService.BeforeDestruction - freeing apiDB'); Logger.Log(4, 'TaskItemService.BeforeDestruction - freeing apiDB');
apiDB.Free; apiDB.Free;
inherited; inherited;
end; end;
function TApiService.GetTaskItems(taskId: string): TTaskItemsResponse; function TTaskItemService.GetTaskItems(taskId: string): TTaskItemsResponse;
var var
taskHeader: TTaskHeader; taskHeader: TTaskHeader;
taskUserOption: TTaskUserOption; taskUserOption: TTaskUserOption;
...@@ -70,7 +71,7 @@ var ...@@ -70,7 +71,7 @@ var
taskApplicationOption: TTaskApplicationOption; taskApplicationOption: TTaskApplicationOption;
item: TTaskItem; item: TTaskItem;
begin begin
Logger.Log(4, Format('ApiService.GetTaskItems - TASK_ID="%s"', [taskId])); Logger.Log(4, Format('TaskItemService.GetTaskItems - TASK_ID="%s"', [taskId]));
Result := TTaskItemsResponse.Create; Result := TTaskItemsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
...@@ -82,7 +83,7 @@ begin ...@@ -82,7 +83,7 @@ begin
if apiDB.uqTaskHeader.IsEmpty then if apiDB.uqTaskHeader.IsEmpty then
begin begin
Logger.Log(2, Format('ApiService.GetTaskItems - no task header found for TASK_ID="%s"', [taskId])); Logger.Log(2, Format('TaskItemService.GetTaskItems - no task header found for TASK_ID="%s"', [taskId]));
Result.count := 0; Result.count := 0;
Exit; Exit;
end; end;
...@@ -205,23 +206,23 @@ begin ...@@ -205,23 +206,23 @@ begin
end; end;
Result.count := Result.items.Count; Result.count := Result.items.Count;
Logger.Log(4, Format('ApiService.GetTaskItems - returned %d item(s)', [Result.count])); Logger.Log(4, Format('TaskItemService.GetTaskItems - returned %d item(s)', [Result.count]));
except except
on E: Exception do on E: Exception do
begin begin
Logger.Log(2, 'ApiService.GetTaskItems - ERROR: ' + E.Message); Logger.Log(2, 'TaskItemService.GetTaskItems - ERROR: ' + E.Message);
raise; raise;
end; end;
end; end;
end; end;
function TApiService.AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean; function TTaskItemService.AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
var var
newItemNum: Integer; newItemNum: Integer;
maxItemNum: Integer; maxItemNum: Integer;
begin begin
Logger.Log(4, Format('ApiService.AddTaskRow - TASK_ID="%s" INSERT_AFTER_ITEM_NUM="%d"', [taskId, insertAfterItemNum])); Logger.Log(4, Format('TaskItemService.AddTaskRow - TASK_ID="%s" INSERT_AFTER_ITEM_NUM="%d"', [taskId, insertAfterItemNum]));
apiDB.uqGetTaskMaxItemNum.Close; apiDB.uqGetTaskMaxItemNum.Close;
apiDB.uqGetTaskMaxItemNum.ParamByName('TASK_ID').AsString := taskId; apiDB.uqGetTaskMaxItemNum.ParamByName('TASK_ID').AsString := taskId;
...@@ -257,21 +258,21 @@ begin ...@@ -257,21 +258,21 @@ begin
apiDB.uqAddTaskRow.Connection.Commit; apiDB.uqAddTaskRow.Connection.Commit;
Result := True; Result := True;
Logger.Log(4, Format('ApiService.AddTaskRow - OK TASK_ID="%s" ITEM_NUM="%d"', [taskId, newItemNum])); Logger.Log(4, Format('TaskItemService.AddTaskRow - OK TASK_ID="%s" ITEM_NUM="%d"', [taskId, newItemNum]));
except except
on E: Exception do on E: Exception do
begin begin
if apiDB.uqAddTaskRow.Connection.InTransaction then if apiDB.uqAddTaskRow.Connection.InTransaction then
apiDB.uqAddTaskRow.Connection.Rollback; apiDB.uqAddTaskRow.Connection.Rollback;
Logger.Log(2, 'ApiService.AddTaskRow - ERROR: ' + E.Message); Logger.Log(2, 'TaskItemService.AddTaskRow - ERROR: ' + E.Message);
raise; raise;
end; end;
end; end;
end; end;
procedure TApiService.MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer); procedure TTaskItemService.MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
var var
oldItemNum: Integer; oldItemNum: Integer;
maxItemNum: Integer; maxItemNum: Integer;
...@@ -368,7 +369,7 @@ begin ...@@ -368,7 +369,7 @@ begin
end; end;
function TApiService.SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean; function TTaskItemService.SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
var var
uqSaveField: TUniQuery; uqSaveField: TUniQuery;
columnName: string; columnName: string;
...@@ -478,7 +479,7 @@ begin ...@@ -478,7 +479,7 @@ begin
end; end;
function TApiService.BuildTaskNumber: string; function TTaskItemService.BuildTaskNumber: string;
procedure AddPart(const value: string); procedure AddPart(const value: string);
var var
...@@ -506,7 +507,7 @@ begin ...@@ -506,7 +507,7 @@ begin
end; end;
function TApiService.BuildTaskTitle(const taskNumber, projectName, subject: string): string; function TTaskItemService.BuildTaskTitle(const taskNumber, projectName, subject: string): string;
begin begin
Result := 'Task'; Result := 'Task';
...@@ -521,11 +522,11 @@ begin ...@@ -521,11 +522,11 @@ begin
end; end;
function TApiService.DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean; function TTaskItemService.DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
var var
oldItemNum: Integer; oldItemNum: Integer;
begin begin
Logger.Log(4, Format('ApiService.DeleteTaskRow - TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId])); Logger.Log(4, Format('TaskItemService.DeleteTaskRow - TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId]));
apiDB.uqGetTaskRowPosition.Close; apiDB.uqGetTaskRowPosition.Close;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ID').AsInteger := taskId; apiDB.uqGetTaskRowPosition.ParamByName('TASK_ID').AsInteger := taskId;
...@@ -534,7 +535,7 @@ begin ...@@ -534,7 +535,7 @@ begin
if apiDB.uqGetTaskRowPosition.IsEmpty then if apiDB.uqGetTaskRowPosition.IsEmpty then
begin begin
Logger.Log(2, Format('ApiService.DeleteTaskRow - row not found TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId])); Logger.Log(2, Format('TaskItemService.DeleteTaskRow - row not found TASK_ID="%d" TASK_ITEM_ID="%d"', [taskId, taskItemId]));
raise Exception.Create('Task row not found.'); raise Exception.Create('Task row not found.');
end; end;
...@@ -553,21 +554,21 @@ begin ...@@ -553,21 +554,21 @@ begin
apiDB.uqGetTaskRowPosition.Connection.Commit; apiDB.uqGetTaskRowPosition.Connection.Commit;
Result := True; Result := True;
Logger.Log(4, Format('ApiService.DeleteTaskRow - OK TASK_ID="%d" TASK_ITEM_ID="%d" OLD_ITEM_NUM="%d"', [taskId, taskItemId, oldItemNum])); Logger.Log(4, Format('TaskItemService.DeleteTaskRow - OK TASK_ID="%d" TASK_ITEM_ID="%d" OLD_ITEM_NUM="%d"', [taskId, taskItemId, oldItemNum]));
except except
on E: Exception do on E: Exception do
begin begin
if apiDB.uqGetTaskRowPosition.Connection.InTransaction then if apiDB.uqGetTaskRowPosition.Connection.InTransaction then
apiDB.uqGetTaskRowPosition.Connection.Rollback; apiDB.uqGetTaskRowPosition.Connection.Rollback;
Logger.Log(2, Format('ApiService.DeleteTaskRow - ERROR TASK_ID="%d" TASK_ITEM_ID="%d" MSG="%s"', [taskId, taskItemId, E.Message])); Logger.Log(2, Format('TaskItemService.DeleteTaskRow - ERROR TASK_ID="%d" TASK_ITEM_ID="%d" MSG="%s"', [taskId, taskItemId, E.Message]));
raise; raise;
end; end;
end; end;
end; end;
function TApiService.FindAssignedOptionId(const taskId, name: string): string; function TTaskItemService.FindAssignedOptionId(const taskId, name: string): string;
begin begin
Result := ''; Result := '';
...@@ -587,7 +588,8 @@ begin ...@@ -587,7 +588,8 @@ begin
end; end;
end; end;
function TApiService.FindAssignedOptionName(const taskId, name: string): string;
function TTaskItemService.FindAssignedOptionName(const taskId, name: string): string;
begin begin
Result := ''; Result := '';
...@@ -608,7 +610,7 @@ begin ...@@ -608,7 +610,7 @@ begin
end; end;
function TApiService.AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; function TTaskItemService.AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
var var
newName: string; newName: string;
existingName: string; existingName: string;
...@@ -646,7 +648,7 @@ begin ...@@ -646,7 +648,7 @@ begin
end; end;
function TApiService.RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse; function TTaskItemService.RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
var var
oldAssignedId: string; oldAssignedId: string;
existingAssignedId: string; existingAssignedId: string;
...@@ -706,7 +708,7 @@ begin ...@@ -706,7 +708,7 @@ begin
end; end;
function TApiService.DeleteAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; function TTaskItemService.DeleteAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
var var
assignedId: string; assignedId: string;
assignedName: string; assignedName: string;
...@@ -743,7 +745,7 @@ begin ...@@ -743,7 +745,7 @@ begin
end; end;
function TApiService.BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse; function TTaskItemService.BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
var var
taskUserOption: TTaskUserOption; taskUserOption: TTaskUserOption;
begin begin
...@@ -767,7 +769,7 @@ begin ...@@ -767,7 +769,7 @@ begin
end; end;
function TApiService.FindProjectId(const taskId: string): string; function TTaskItemService.FindProjectId(const taskId: string): string;
begin begin
Result := ''; Result := '';
...@@ -780,7 +782,7 @@ begin ...@@ -780,7 +782,7 @@ begin
end; end;
function TApiService.FindApplicationId(const taskId, name: string): Integer; function TTaskItemService.FindApplicationId(const taskId, name: string): Integer;
begin begin
Result := 0; Result := 0;
...@@ -800,7 +802,8 @@ begin ...@@ -800,7 +802,8 @@ begin
end; end;
end; end;
function TApiService.FindApplicationName(const taskId, name: string): string;
function TTaskItemService.FindApplicationName(const taskId, name: string): string;
begin begin
Result := ''; Result := '';
...@@ -821,7 +824,7 @@ begin ...@@ -821,7 +824,7 @@ begin
end; end;
function TApiService.BuildApplicationOptionsResponse(const taskId: string): TTaskApplicationOptionsResponse; function TTaskItemService.BuildApplicationOptionsResponse(const taskId: string): TTaskApplicationOptionsResponse;
var var
taskApplicationOption: TTaskApplicationOption; taskApplicationOption: TTaskApplicationOption;
begin begin
...@@ -845,7 +848,7 @@ begin ...@@ -845,7 +848,7 @@ begin
end; end;
function TApiService.AddApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse; function TTaskItemService.AddApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
var var
projectId: string; projectId: string;
newName: string; newName: string;
...@@ -884,7 +887,7 @@ begin ...@@ -884,7 +887,7 @@ begin
end; end;
function TApiService.RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse; function TTaskItemService.RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
var var
projectId: string; projectId: string;
oldApplicationId: Integer; oldApplicationId: Integer;
...@@ -949,7 +952,7 @@ begin ...@@ -949,7 +952,7 @@ begin
end; end;
function TApiService.DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse; function TTaskItemService.DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
var var
projectId: string; projectId: string;
applicationId: Integer; applicationId: Integer;
...@@ -990,7 +993,8 @@ begin ...@@ -990,7 +993,8 @@ begin
Result := BuildApplicationOptionsResponse(taskId); Result := BuildApplicationOptionsResponse(taskId);
end; end;
function TApiService.FindReportedOptionId(const taskId, name: string): string;
function TTaskItemService.FindReportedOptionId(const taskId, name: string): string;
begin begin
Result := ''; Result := '';
...@@ -1010,7 +1014,8 @@ begin ...@@ -1010,7 +1014,8 @@ begin
end; end;
end; end;
function TApiService.FindReportedOptionName(const taskId, name: string): string;
function TTaskItemService.FindReportedOptionName(const taskId, name: string): string;
begin begin
Result := ''; Result := '';
...@@ -1030,7 +1035,8 @@ begin ...@@ -1030,7 +1035,8 @@ begin
end; end;
end; end;
function TApiService.BuildReportedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
function TTaskItemService.BuildReportedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
var var
taskUserOption: TTaskUserOption; taskUserOption: TTaskUserOption;
begin begin
...@@ -1053,7 +1059,8 @@ begin ...@@ -1053,7 +1059,8 @@ begin
end; end;
end; end;
function TApiService.AddReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.AddReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
var var
newName: string; newName: string;
existingName: string; existingName: string;
...@@ -1090,7 +1097,8 @@ begin ...@@ -1090,7 +1097,8 @@ begin
Result := BuildReportedOptionsResponse(taskId); Result := BuildReportedOptionsResponse(taskId);
end; end;
function TApiService.RenameReportedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
function TTaskItemService.RenameReportedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
var var
oldReportedId: string; oldReportedId: string;
existingReportedId: string; existingReportedId: string;
...@@ -1149,7 +1157,8 @@ begin ...@@ -1149,7 +1157,8 @@ begin
Result := BuildReportedOptionsResponse(taskId); Result := BuildReportedOptionsResponse(taskId);
end; end;
function TApiService.DeleteReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.DeleteReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
var var
reportedId: string; reportedId: string;
reportedName: string; reportedName: string;
...@@ -1185,11 +1194,8 @@ begin ...@@ -1185,11 +1194,8 @@ begin
Result := BuildReportedOptionsResponse(taskId); Result := BuildReportedOptionsResponse(taskId);
end; end;
initialization initialization
RegisterServiceType(TypeInfo(IApiService)); RegisterServiceType(TTaskItemService);
RegisterServiceType(TApiService);
end. end.
unit TimeEntry.Service;
interface
uses
System.Generics.Collections,
XData.Service.Common,
Bcl.Types.Nullable,
Aurelius.Mapping.Attributes;
const
API_MODEL = 'Api';
type
TTimeEntry = class
public
entryId: Integer;
taskDate: string;
taskId: string;
taskDisplay: string;
hours: Nullable<Double>;
taskTime: string;
category: string;
categoryDesc: string;
summary: string;
end;
TTimeEntryTaskOption = class
public
taskId: string;
taskDisplay: string;
end;
TTimeEntryCategoryOption = class
public
code: string;
codeDesc: string;
end;
TTimeEntriesResponse = class
public
userName: string;
count: Integer;
items: TList<TTimeEntry>;
taskOptions: TList<TTimeEntryTaskOption>;
categoryOptions: TList<TTimeEntryCategoryOption>;
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;
end;
implementation
constructor TTimeEntriesResponse.Create;
begin
inherited;
items := TList<TTimeEntry>.Create;
taskOptions := TList<TTimeEntryTaskOption>.Create;
categoryOptions := TList<TTimeEntryCategoryOption>.Create;
end;
destructor TTimeEntriesResponse.Destroy;
begin
items.Free;
taskOptions.Free;
categoryOptions.Free;
inherited;
end;
initialization
RegisterServiceType(TypeInfo(ITimeEntryService));
end.
unit TimeEntry.ServiceImpl;
interface
uses
XData.Service.Common,
XData.Server.Module,
TimeEntry.Service,
Api.Database,
Uni;
type
[ServiceImplementation]
TTimeEntryService = class(TInterfacedObject, ITimeEntryService)
strict private
apiDB: TApiDatabase;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function ParseIsoDate(value: string): TDateTime;
function FieldText(AQuery: TUniQuery; AFieldName: string): string;
function BuildTaskNumber(AQuery: TUniQuery): string;
function BuildTaskDisplay(AQuery: TUniQuery): string;
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 LoadCategoryOptions(AResponse: TTimeEntriesResponse);
function GetNextIdValue(const AKeyName: string): string;
public
function GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
function AddTimeEntry(userId, taskDate: string): string;
end;
implementation
uses
System.SysUtils,
System.DateUtils,
Common.Logging;
procedure TTimeEntryService.AfterConstruction;
begin
inherited;
apiDB := TApiDatabase.Create(nil);
end;
procedure TTimeEntryService.BeforeDestruction;
begin
apiDB.Free;
inherited;
end;
function TTimeEntryService.ParseIsoDate(value: string): TDateTime;
var
yearValue: Integer;
monthValue: Integer;
dayValue: Integer;
begin
yearValue := StrToInt(Copy(value, 1, 4));
monthValue := StrToInt(Copy(value, 6, 2));
dayValue := StrToInt(Copy(value, 9, 2));
Result := EncodeDate(yearValue, monthValue, dayValue);
end;
function TTimeEntryService.FieldText(AQuery: TUniQuery; AFieldName: string): string;
begin
if AQuery.FieldByName(AFieldName).IsNull then
Result := ''
else
Result := Trim(AQuery.FieldByName(AFieldName).AsString);
end;
function TTimeEntryService.BuildTaskNumber(AQuery: TUniQuery): string;
procedure AddPart(AValue: string);
begin
if AValue = '' then
Exit;
if Result <> '' then
Result := Result + '.';
Result := Result + AValue;
end;
begin
Result := '';
AddPart(FieldText(AQuery, 'TASK_NUM_1'));
AddPart(FieldText(AQuery, 'TASK_NUM_2'));
AddPart(FieldText(AQuery, 'TASK_NUM_3'));
AddPart(FieldText(AQuery, 'TASK_NUM_4'));
AddPart(FieldText(AQuery, 'TASK_NUM_5'));
AddPart(FieldText(AQuery, 'TASK_NUM_6'));
end;
function TTimeEntryService.BuildTaskDisplay(AQuery: TUniQuery): string;
var
customerText: string;
projectText: string;
taskNumberText: string;
subjectText: string;
leftText: string;
taskText: string;
begin
customerText := FieldText(AQuery, 'CUSTOMER_SHORT_NAME');
projectText := FieldText(AQuery, 'PROJECT_NAME');
taskNumberText := BuildTaskNumber(AQuery);
subjectText := FieldText(AQuery, 'TASK_SUBJECT');
leftText := customerText;
if projectText <> '' then
begin
if leftText <> '' then
leftText := leftText + ' - ';
leftText := leftText + projectText;
end;
taskText := taskNumberText;
if subjectText <> '' then
begin
if taskText <> '' then
taskText := taskText + ' - ';
taskText := taskText + subjectText;
end;
Result := leftText;
if taskText <> '' then
begin
if Result <> '' then
Result := Result + ' | ';
Result := Result + taskText;
end;
end;
procedure TTimeEntryService.LoadUserName(AResponse: TTimeEntriesResponse; userId: string);
var
userQuery: TUniQuery;
begin
userQuery := TUniQuery.Create(nil);
try
userQuery.Connection := apiDB.ucETaskApi;
userQuery.SQL.Text :=
'select USER_ID, NAME, USER_NAME ' +
'from users ' +
'where USER_ID = :USER_ID';
userQuery.ParamByName('USER_ID').AsString := userId;
userQuery.Open;
AResponse.userName := FieldText(userQuery, 'NAME');
if AResponse.userName = '' then
AResponse.userName := FieldText(userQuery, 'USER_NAME');
finally
userQuery.Free;
end;
end;
procedure TTimeEntryService.LoadTimeEntryItems(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
var
itemsQuery: TUniQuery;
item: TTimeEntry;
begin
itemsQuery := TUniQuery.Create(nil);
try
itemsQuery.Connection := apiDB.ucETaskApi;
itemsQuery.SQL.Text :=
'select ' +
' ti.ENTRY_ID, ' +
' ti.TASK_DATE, ' +
' ti.TASK_ID, ' +
' ti.HOURS, ' +
' ti.TASK_TIME, ' +
' ti.CATEGORY, ' +
' cat.CODE_DESC as CATEGORY_DESC, ' +
' ti.SUMMARY, ' +
' 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 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 ' +
'where ti.USER_ID = :USER_ID ' +
' and ti.TASK_DATE >= :START_DATE ' +
' and ti.TASK_DATE < :END_DATE ' +
'order by ti.TASK_DATE, ti.ENTRY_ID';
itemsQuery.ParamByName('USER_ID').AsString := userId;
itemsQuery.ParamByName('START_DATE').AsDateTime := startDateValue;
itemsQuery.ParamByName('END_DATE').AsDateTime := exclusiveEndDateValue;
itemsQuery.Open;
while not itemsQuery.Eof do
begin
item := TTimeEntry.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(item);
item.entryId := itemsQuery.FieldByName('ENTRY_ID').AsInteger;
if itemsQuery.FieldByName('TASK_DATE').IsNull then
item.taskDate := ''
else
item.taskDate := FormatDateTime('yyyy-mm-dd', itemsQuery.FieldByName('TASK_DATE').AsDateTime);
item.taskId := FieldText(itemsQuery, 'TASK_ID');
item.taskDisplay := BuildTaskDisplay(itemsQuery);
if not itemsQuery.FieldByName('HOURS').IsNull then
item.hours := itemsQuery.FieldByName('HOURS').AsFloat;
item.taskTime := FieldText(itemsQuery, 'TASK_TIME');
item.category := FieldText(itemsQuery, 'CATEGORY');
item.categoryDesc := FieldText(itemsQuery, 'CATEGORY_DESC');
if item.categoryDesc = '' then
item.categoryDesc := item.category;
item.summary := FieldText(itemsQuery, 'SUMMARY');
AResponse.items.Add(item);
itemsQuery.Next;
end;
finally
itemsQuery.Free;
end;
end;
procedure TTimeEntryService.LoadTaskOptions(AResponse: TTimeEntriesResponse; userId: string; startDateValue, exclusiveEndDateValue: TDateTime);
var
tasksQuery: TUniQuery;
taskOption: TTimeEntryTaskOption;
begin
tasksQuery := TUniQuery.Create(nil);
try
tasksQuery.Connection := apiDB.ucETaskApi;
tasksQuery.SQL.Text :=
'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 ' +
'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 ' +
' ) ' +
') ' +
'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
begin
taskOption := TTimeEntryTaskOption.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(taskOption);
taskOption.taskId := FieldText(tasksQuery, 'TASK_ID');
taskOption.taskDisplay := BuildTaskDisplay(tasksQuery);
AResponse.taskOptions.Add(taskOption);
tasksQuery.Next;
end;
finally
tasksQuery.Free;
end;
end;
procedure TTimeEntryService.LoadCategoryOptions(AResponse: TTimeEntriesResponse);
var
categoriesQuery: TUniQuery;
categoryOption: TTimeEntryCategoryOption;
begin
categoriesQuery := TUniQuery.Create(nil);
try
categoriesQuery.Connection := apiDB.ucETaskApi;
categoriesQuery.SQL.Text :=
'select CODE, CODE_DESC ' +
'from codes ' +
'where CATEGORY = ''TASK'' ' +
' and CODE_TYPE = ''CATGRY'' ' +
'order by CODE_DESC';
categoriesQuery.Open;
while not categoriesQuery.Eof do
begin
categoryOption := TTimeEntryCategoryOption.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(categoryOption);
categoryOption.code := FieldText(categoriesQuery, 'CODE');
categoryOption.codeDesc := FieldText(categoriesQuery, 'CODE_DESC');
AResponse.categoryOptions.Add(categoryOption);
categoriesQuery.Next;
end;
finally
categoriesQuery.Free;
end;
end;
function TTimeEntryService.GetTimeEntries(userId, startDate, endDate: string): TTimeEntriesResponse;
var
startDateValue: TDateTime;
endDateValue: TDateTime;
exclusiveEndDateValue: TDateTime;
begin
Logger.Log(4, Format('TimeEntryService.GetTimeEntries - USER_ID="%s" START="%s" END="%s"', [userId, startDate, endDate]));
Result := TTimeEntriesResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
startDateValue := ParseIsoDate(startDate);
endDateValue := ParseIsoDate(endDate);
exclusiveEndDateValue := IncDay(endDateValue, 1);
LoadUserName(Result, userId);
LoadTimeEntryItems(Result, userId, startDateValue, exclusiveEndDateValue);
LoadTaskOptions(Result, userId, startDateValue, exclusiveEndDateValue);
LoadCategoryOptions(Result);
Result.count := Result.items.Count;
Logger.Log(4, Format('TimeEntryService.GetTimeEntries - returned %d item(s)', [Result.count]));
end;
function TTimeEntryService.GetNextIdValue(const AKeyName: string): string;
var
q: TUniQuery;
nextId: Integer;
begin
q := TUniQuery.Create(nil);
try
q.Connection := apiDB.ucETaskApi;
q.SQL.Text :=
'select KeyValue from idfield where KeyName = :KEY_NAME';
q.ParamByName('KEY_NAME').AsString := AKeyName;
q.Open;
nextId := q.FieldByName('KeyValue').AsInteger + 1;
q.Close;
q.SQL.Text :=
'update idfield set KeyValue = :KEY_VALUE where KeyName = :KEY_NAME';
q.ParamByName('KEY_VALUE').AsInteger := nextId;
q.ParamByName('KEY_NAME').AsString := AKeyName;
q.ExecSQL;
Result := IntToStr(nextId);
finally
q.Free;
end;
end;
function TTimeEntryService.AddTimeEntry(userId, taskDate: string): string;
var
taskDateValue: TDateTime;
entryId: string;
begin
Logger.Log(4, Format('TimeEntryService.AddTimeEntry - USER_ID="%s" TASK_DATE="%s"', [userId, taskDate]));
taskDateValue := ParseIsoDate(taskDate);
entryId := GetNextIdValue('TimeEntryId');
apiDB.uqAddTimeEntry.Close;
apiDB.uqAddTimeEntry.ParamByName('ENTRY_ID').AsString := entryId;
apiDB.uqAddTimeEntry.ParamByName('USER_ID').AsString := userId;
apiDB.uqAddTimeEntry.ParamByName('TASK_DATE').AsDateTime := taskDateValue;
apiDB.uqAddTimeEntry.ParamByName('CREATED_BY').AsString := userId;
apiDB.uqAddTimeEntry.ParamByName('MODIFIED_BY').AsString := userId;
apiDB.uqAddTimeEntry.ExecSQL;
Result := entryId;
Logger.Log(4, 'TimeEntryService.AddTimeEntry - new ENTRY_ID=' + Result);
end;
initialization
RegisterServiceType(TTimeEntryService);
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=190 LogFileNum=219
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.131
......
...@@ -20,9 +20,11 @@ uses ...@@ -20,9 +20,11 @@ uses
Auth.Service in 'Source\Auth.Service.pas', Auth.Service in 'Source\Auth.Service.pas',
Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas', Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas',
App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule}, App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule},
Api.Service in 'Source\Api.Service.pas', TaskItem.Service in 'Source\TaskItem.Service.pas',
Api.ServiceImpl in 'Source\Api.ServiceImpl.pas', TaskItem.ServiceImpl in 'Source\TaskItem.ServiceImpl.pas',
Common.Ini in 'Source\Common.Ini.pas'; Common.Ini in 'Source\Common.Ini.pas',
TimeEntry.Service in 'Source\TimeEntry.Service.pas',
TimeEntry.ServiceImpl in 'Source\TimeEntry.ServiceImpl.pas';
type type
TMemoLogAppender = class( TInterfacedObject, ILogAppender ) TMemoLogAppender = class( TInterfacedObject, ILogAppender )
......
...@@ -175,9 +175,11 @@ ...@@ -175,9 +175,11 @@
<Form>AppServerModule</Form> <Form>AppServerModule</Form>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="Source\Api.Service.pas"/> <DCCReference Include="Source\TaskItem.Service.pas"/>
<DCCReference Include="Source\Api.ServiceImpl.pas"/> <DCCReference Include="Source\TaskItem.ServiceImpl.pas"/>
<DCCReference Include="Source\Common.Ini.pas"/> <DCCReference Include="Source\Common.Ini.pas"/>
<DCCReference Include="Source\TimeEntry.Service.pas"/>
<DCCReference Include="Source\TimeEntry.ServiceImpl.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
......
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