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
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddTaskRow', [FTaskId, insertAfterItemNum]
'ITaskItemService.AddTaskRow', [FTaskId, insertAfterItemNum]
));
console.log('AddTaskRow response=' + string(TJSJSON.stringify(response.Result)));
......@@ -661,13 +661,13 @@ var
titleText: string;
rowCount: Integer;
begin
console.log('IApiService.GetTaskItems called with task_id: ' + ATaskId);
console.log('ITaskItemService.GetTaskItems called with task_id: ' + ATaskId);
console.log('Load Tasks Fired');
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.GetTaskItems', [ATaskId]
'ITaskItemService.GetTaskItems', [ATaskId]
));
except
on E: EXDataClientRequestException do
......@@ -1031,7 +1031,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow',
'ITaskItemService.MoveTaskRow',
[
StrToIntDef(xdwdsTaskstaskId.AsString, 0),
movedTaskItemId,
......@@ -1106,7 +1106,7 @@ begin
Exit;
try
response := await(xdwcTasks.RawInvokeAsync('IApiService.SaveTaskItemField', [payload]));
response := await(xdwcTasks.RawInvokeAsync('ITaskItemService.SaveTaskItemField', [payload]));
console.log('SaveField: response=' + string(TJSJSON.stringify(response.Result)));
except
on E: EXDataClientRequestException do
......@@ -1129,7 +1129,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteTaskRow',
'ITaskItemService.DeleteTaskRow',
[FSelectedTaskId, FSelectedTaskItemId]
));
console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result)));
......@@ -1323,7 +1323,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddAssignedName',
'ITaskItemService.AddAssignedName',
[FTaskId, Trim(AName)]
));
......@@ -1349,7 +1349,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameAssignedName',
'ITaskItemService.RenameAssignedName',
[FTaskId, Trim(AOldName), Trim(ANewName)]
));
......@@ -1375,7 +1375,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteAssignedName',
'ITaskItemService.DeleteAssignedName',
[FTaskId, Trim(AName)]
));
......@@ -1500,7 +1500,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddApplicationName',
'ITaskItemService.AddApplicationName',
[FTaskId, Trim(AName)]
));
......@@ -1526,7 +1526,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameApplicationName',
'ITaskItemService.RenameApplicationName',
[FTaskId, Trim(AOldName), Trim(ANewName)]
));
......@@ -1552,7 +1552,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteApplicationName',
'ITaskItemService.DeleteApplicationName',
[FTaskId, Trim(AName)]
));
......@@ -1579,7 +1579,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddReportedName',
'ITaskItemService.AddReportedName',
[FTaskId, Trim(AName)]
));
......@@ -1605,7 +1605,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.RenameReportedName',
'ITaskItemService.RenameReportedName',
[FTaskId, Trim(AOldName), Trim(ANewName)]
));
......@@ -1631,7 +1631,7 @@ begin
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteReportedName',
'ITaskItemService.DeleteReportedName',
[FTaskId, Trim(AName)]
));
......
......@@ -2,4 +2,100 @@ object FTimeEntries: TFTimeEntries
Width = 640
Height = 480
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
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>TMS Web Project</title>
<style>
</style>
</head>
<body>
</body>
</html>
\ No newline at end of file
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0 gap-3">
<h5 class="mb-0 text-nowrap" id="lbl_time_entries_title"></h5>
<div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
<div id="lbl_time_total_rows" class="me-2 text-nowrap"></div>
<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">
</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;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs;
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;
type
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 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 declarations }
end;
var
......@@ -19,6 +63,642 @@ var
implementation
uses
Auth.Service,
Utils;
{$R *.dfm}
end.
\ No newline at end of file
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.
is-invalid .form-check-input {
.is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
......@@ -7,14 +7,14 @@ is-invalid .form-check-input {
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
background-color: #204d74 !important;
border-color: #204d74 !important;
}
@keyframes slideInLeft {
......@@ -22,6 +22,7 @@ is-invalid .form-check-input {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
......@@ -39,37 +40,6 @@ is-invalid .form-check-input {
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;
......@@ -89,214 +59,6 @@ span.card {
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;
}
.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
if SameText(timeEntriesParam, 'true') then
begin
if AuthService.Authenticated and not AuthService.TokenExpired then
begin
DisplayMainView;
Exit;
end;
DisplayLoginView('', '', '');
Exit;
end;
......
......@@ -99,7 +99,7 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer>
<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>
<TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
......@@ -160,6 +160,8 @@
<None Include="css\app.css"/>
<None Include="config\config.json"/>
<None Include="css\spinner.css"/>
<None Include="css\task-items.css"/>
<None Include="css\time-entries.css"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
......@@ -240,6 +242,18 @@
<Overwrite>true</Overwrite>
</Platform>
</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">
<Platform Name="Win32">
......
......@@ -10,6 +10,8 @@
<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="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"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
......@@ -17,7 +19,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.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>
<noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script>
......@@ -27,3 +29,5 @@
object ApiDatabase: TApiDatabase
OnCreate = DataModuleCreate
Height = 475
Width = 996
Height = 632
Width = 994
object ucETaskApi: TUniConnection
AutoCommit = False
ProviderName = 'MySQL'
Database = 'eTask'
LoginPrompt = False
Left = 267
Top = 395
Left = 435
Top = 359
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 416
Top = 398
Left = 546
Top = 356
end
object uqUsers: TUniQuery
Connection = ucETaskApi
......@@ -142,7 +142,7 @@ object ApiDatabase: TApiDatabase
'from task_items'
'where TASK_ID = :TASK_ID'
'order by ITEM_NUM')
Left = 56
Left = 58
Top = 26
ParamData = <
item
......@@ -282,7 +282,7 @@ object ApiDatabase: TApiDatabase
'left join project p'
' on p.PROJECT_ID = t.PROJECT_ID'
'where t.TASK_ID = :TASK_ID')
Left = 54
Left = 60
Top = 82
ParamData = <
item
......@@ -367,7 +367,7 @@ object ApiDatabase: TApiDatabase
'from task_items'
'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID')
Left = 240
Left = 236
Top = 16
ParamData = <
item
......@@ -1085,4 +1085,61 @@ object ApiDatabase: TApiDatabase
Value = nil
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
......@@ -88,6 +88,7 @@ type
uqRenameTaskItemReportedBy: TUniQuery;
uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField;
uqProjectReportedUsersNAME: TStringField;
uqAddTimeEntry: TUniQuery;
procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet);
private
......
......@@ -53,7 +53,7 @@ uses
XData.Sys.Exceptions,
Common.Logging,
Common.Middleware.Logging,
Common.Config, Vcl.Forms, IniFiles, Api.Service;
Common.Config, Vcl.Forms, IniFiles, TaskItem.Service, TimeEntry.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'}
......
......@@ -299,7 +299,7 @@ begin
try
jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
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_name', Self.userName);
......
unit Main;
//Authors:
//Elias Sarraf
//Mac Stephens
//Cameron Hayes
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles,
Auth.Service, Auth.Server.Module, Api.Service, Api.Server.Module, App.Server.Module,
ExeInfo;
TaskItem.Service, Auth.Server.Module, Auth.Service, Api.Server.Module, App.Server.Module,
TimeEntry.Service, TimeEntry.ServiceImpl, ExeInfo;
type
TFMain = class(TForm)
......
unit Api.Service;
unit TaskItem.Service;
interface
......@@ -115,10 +115,10 @@ type
type
[ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable)
ITaskItemService = interface(IInvokable)
['{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 AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
......@@ -134,9 +134,10 @@ type
[HttpPost] function RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
[HttpPost] function DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
[HttpPost] procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
[HttpPost] function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
[HttpPost] function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
end;
implementation
......@@ -188,6 +189,6 @@ begin
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
RegisterServiceType(TypeInfo(ITaskItemService));
end.
unit Api.ServiceImpl;
unit TaskItem.ServiceImpl;
interface
uses
XData.Server.Module, XData.Service.Common,
System.Variants, System.DateUtils, Uni,
Api.Service, Api.Database, Common.Logging,
TaskItem.Service, Api.Database, Common.Logging,
System.SysUtils, JSON, System.IniFiles, DBAccess;
type
[ServiceImplementation]
TApiService = class(TInterfacedObject, IApiService)
TTaskItemService = class(TInterfacedObject, ITaskItemService)
strict private
apiDB: TApiDatabase;
private
......@@ -47,22 +47,23 @@ type
implementation
procedure TApiService.AfterConstruction;
procedure TTaskItemService.AfterConstruction;
begin
inherited;
apiDB := TApiDatabase.Create(nil);
Logger.Log(4, 'ApiService.AfterConstruction - apiDB created');
Logger.Log(4, 'TaskItemService.AfterConstruction - apiDB created');
end;
procedure TApiService.BeforeDestruction;
procedure TTaskItemService.BeforeDestruction;
begin
Logger.Log(4, 'ApiService.BeforeDestruction - freeing apiDB');
Logger.Log(4, 'TaskItemService.BeforeDestruction - freeing apiDB');
apiDB.Free;
inherited;
end;
function TApiService.GetTaskItems(taskId: string): TTaskItemsResponse;
function TTaskItemService.GetTaskItems(taskId: string): TTaskItemsResponse;
var
taskHeader: TTaskHeader;
taskUserOption: TTaskUserOption;
......@@ -70,7 +71,7 @@ var
taskApplicationOption: TTaskApplicationOption;
item: TTaskItem;
begin
Logger.Log(4, Format('ApiService.GetTaskItems - TASK_ID="%s"', [taskId]));
Logger.Log(4, Format('TaskItemService.GetTaskItems - TASK_ID="%s"', [taskId]));
Result := TTaskItemsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
......@@ -82,7 +83,7 @@ begin
if apiDB.uqTaskHeader.IsEmpty then
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;
Exit;
end;
......@@ -205,23 +206,23 @@ begin
end;
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
on E: Exception do
begin
Logger.Log(2, 'ApiService.GetTaskItems - ERROR: ' + E.Message);
Logger.Log(2, 'TaskItemService.GetTaskItems - ERROR: ' + E.Message);
raise;
end;
end;
end;
function TApiService.AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
function TTaskItemService.AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
var
newItemNum: Integer;
maxItemNum: Integer;
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.ParamByName('TASK_ID').AsString := taskId;
......@@ -257,21 +258,21 @@ begin
apiDB.uqAddTaskRow.Connection.Commit;
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
on E: Exception do
begin
if apiDB.uqAddTaskRow.Connection.InTransaction then
apiDB.uqAddTaskRow.Connection.Rollback;
Logger.Log(2, 'ApiService.AddTaskRow - ERROR: ' + E.Message);
Logger.Log(2, 'TaskItemService.AddTaskRow - ERROR: ' + E.Message);
raise;
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
oldItemNum: Integer;
maxItemNum: Integer;
......@@ -368,7 +369,7 @@ begin
end;
function TApiService.SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
function TTaskItemService.SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
var
uqSaveField: TUniQuery;
columnName: string;
......@@ -478,7 +479,7 @@ begin
end;
function TApiService.BuildTaskNumber: string;
function TTaskItemService.BuildTaskNumber: string;
procedure AddPart(const value: string);
var
......@@ -506,7 +507,7 @@ begin
end;
function TApiService.BuildTaskTitle(const taskNumber, projectName, subject: string): string;
function TTaskItemService.BuildTaskTitle(const taskNumber, projectName, subject: string): string;
begin
Result := 'Task';
......@@ -521,11 +522,11 @@ begin
end;
function TApiService.DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
function TTaskItemService.DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
var
oldItemNum: Integer;
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.ParamByName('TASK_ID').AsInteger := taskId;
......@@ -534,7 +535,7 @@ begin
if apiDB.uqGetTaskRowPosition.IsEmpty then
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.');
end;
......@@ -553,21 +554,21 @@ begin
apiDB.uqGetTaskRowPosition.Connection.Commit;
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
on E: Exception do
begin
if apiDB.uqGetTaskRowPosition.Connection.InTransaction then
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;
end;
end;
end;
function TApiService.FindAssignedOptionId(const taskId, name: string): string;
function TTaskItemService.FindAssignedOptionId(const taskId, name: string): string;
begin
Result := '';
......@@ -587,7 +588,8 @@ begin
end;
end;
function TApiService.FindAssignedOptionName(const taskId, name: string): string;
function TTaskItemService.FindAssignedOptionName(const taskId, name: string): string;
begin
Result := '';
......@@ -608,7 +610,7 @@ begin
end;
function TApiService.AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
var
newName: string;
existingName: string;
......@@ -646,7 +648,7 @@ begin
end;
function TApiService.RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
function TTaskItemService.RenameAssignedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
var
oldAssignedId: string;
existingAssignedId: string;
......@@ -706,7 +708,7 @@ begin
end;
function TApiService.DeleteAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.DeleteAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
var
assignedId: string;
assignedName: string;
......@@ -743,7 +745,7 @@ begin
end;
function TApiService.BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
function TTaskItemService.BuildAssignedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
var
taskUserOption: TTaskUserOption;
begin
......@@ -767,7 +769,7 @@ begin
end;
function TApiService.FindProjectId(const taskId: string): string;
function TTaskItemService.FindProjectId(const taskId: string): string;
begin
Result := '';
......@@ -780,7 +782,7 @@ begin
end;
function TApiService.FindApplicationId(const taskId, name: string): Integer;
function TTaskItemService.FindApplicationId(const taskId, name: string): Integer;
begin
Result := 0;
......@@ -800,7 +802,8 @@ begin
end;
end;
function TApiService.FindApplicationName(const taskId, name: string): string;
function TTaskItemService.FindApplicationName(const taskId, name: string): string;
begin
Result := '';
......@@ -821,7 +824,7 @@ begin
end;
function TApiService.BuildApplicationOptionsResponse(const taskId: string): TTaskApplicationOptionsResponse;
function TTaskItemService.BuildApplicationOptionsResponse(const taskId: string): TTaskApplicationOptionsResponse;
var
taskApplicationOption: TTaskApplicationOption;
begin
......@@ -845,7 +848,7 @@ begin
end;
function TApiService.AddApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
function TTaskItemService.AddApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
var
projectId: string;
newName: string;
......@@ -884,7 +887,7 @@ begin
end;
function TApiService.RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
function TTaskItemService.RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
var
projectId: string;
oldApplicationId: Integer;
......@@ -949,7 +952,7 @@ begin
end;
function TApiService.DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
function TTaskItemService.DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
var
projectId: string;
applicationId: Integer;
......@@ -990,7 +993,8 @@ begin
Result := BuildApplicationOptionsResponse(taskId);
end;
function TApiService.FindReportedOptionId(const taskId, name: string): string;
function TTaskItemService.FindReportedOptionId(const taskId, name: string): string;
begin
Result := '';
......@@ -1010,7 +1014,8 @@ begin
end;
end;
function TApiService.FindReportedOptionName(const taskId, name: string): string;
function TTaskItemService.FindReportedOptionName(const taskId, name: string): string;
begin
Result := '';
......@@ -1030,7 +1035,8 @@ begin
end;
end;
function TApiService.BuildReportedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
function TTaskItemService.BuildReportedOptionsResponse(const taskId: string): TTaskUserOptionsResponse;
var
taskUserOption: TTaskUserOption;
begin
......@@ -1053,7 +1059,8 @@ begin
end;
end;
function TApiService.AddReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.AddReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
var
newName: string;
existingName: string;
......@@ -1090,7 +1097,8 @@ begin
Result := BuildReportedOptionsResponse(taskId);
end;
function TApiService.RenameReportedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
function TTaskItemService.RenameReportedName(taskId: string; oldName: string; newName: string): TTaskUserOptionsResponse;
var
oldReportedId: string;
existingReportedId: string;
......@@ -1149,7 +1157,8 @@ begin
Result := BuildReportedOptionsResponse(taskId);
end;
function TApiService.DeleteReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
function TTaskItemService.DeleteReportedName(taskId: string; name: string): TTaskUserOptionsResponse;
var
reportedId: string;
reportedName: string;
......@@ -1185,11 +1194,8 @@ begin
Result := BuildReportedOptionsResponse(taskId);
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
RegisterServiceType(TApiService);
RegisterServiceType(TTaskItemService);
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 @@
MemoLogLevel=4
FileLogLevel=4
webClientVersion=0.8.9
LogFileNum=190
LogFileNum=219
[Database]
Server=192.168.102.131
......
......@@ -20,9 +20,11 @@ uses
Auth.Service in 'Source\Auth.Service.pas',
Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas',
App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule},
Api.Service in 'Source\Api.Service.pas',
Api.ServiceImpl in 'Source\Api.ServiceImpl.pas',
Common.Ini in 'Source\Common.Ini.pas';
TaskItem.Service in 'Source\TaskItem.Service.pas',
TaskItem.ServiceImpl in 'Source\TaskItem.ServiceImpl.pas',
Common.Ini in 'Source\Common.Ini.pas',
TimeEntry.Service in 'Source\TimeEntry.Service.pas',
TimeEntry.ServiceImpl in 'Source\TimeEntry.ServiceImpl.pas';
type
TMemoLogAppender = class( TInterfacedObject, ILogAppender )
......
......@@ -175,9 +175,11 @@
<Form>AppServerModule</Form>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="Source\Api.Service.pas"/>
<DCCReference Include="Source\Api.ServiceImpl.pas"/>
<DCCReference Include="Source\TaskItem.Service.pas"/>
<DCCReference Include="Source\TaskItem.ServiceImpl.pas"/>
<DCCReference Include="Source\Common.Ini.pas"/>
<DCCReference Include="Source\TimeEntry.Service.pas"/>
<DCCReference Include="Source\TimeEntry.ServiceImpl.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</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