Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
emT3web
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mac Stephens
emT3web
Commits
29263ec6
Commit
29263ec6
authored
May 13, 2026
by
Mac Stephens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add row validation, saving
parent
f6aacf29
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
557 additions
and
36 deletions
+557
-36
View.TimeEntries.dfm
emT3Web/View.TimeEntries.dfm
+10
-0
View.TimeEntries.html
emT3Web/View.TimeEntries.html
+1
-0
View.TimeEntries.pas
emT3Web/View.TimeEntries.pas
+428
-35
time-entries.css
emT3Web/css/time-entries.css
+6
-0
Api.Database.dfm
emT3XDataServer/Source/Api.Database.dfm
+64
-0
Api.Database.pas
emT3XDataServer/Source/Api.Database.pas
+1
-0
TimeEntry.Service.pas
emT3XDataServer/Source/TimeEntry.Service.pas
+13
-0
TimeEntry.ServiceImpl.pas
emT3XDataServer/Source/TimeEntry.ServiceImpl.pas
+33
-0
emT3XDataServer.ini
emT3XDataServer/bin/emT3XDataServer.ini
+1
-1
No files found.
emT3Web/View.TimeEntries.dfm
View file @
29263ec6
...
@@ -3,6 +3,16 @@ object FTimeEntries: TFTimeEntries
...
@@ -3,6 +3,16 @@ object FTimeEntries: TFTimeEntries
Height = 480
Height = 480
ElementFont = efCSS
ElementFont = efCSS
OnCreate = WebFormCreate
OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel
Left = 90
Top = 32
Width = 69
Height = 15
ElementID = 'lbl_validation_message'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object edtWeekOf: TWebEdit
object edtWeekOf: TWebEdit
Left = 71
Left = 71
Top = 80
Top = 80
...
...
emT3Web/View.TimeEntries.html
View file @
29263ec6
...
@@ -23,5 +23,6 @@
...
@@ -23,5 +23,6 @@
<button
id=
"btn_add_entry"
class=
"btn btn-sm btn-success text-nowrap"
>
Add Entry
</button>
<button
id=
"btn_add_entry"
class=
"btn btn-sm btn-success text-nowrap"
>
Add Entry
</button>
</div>
</div>
</div>
</div>
<div
id=
"lbl_validation_message"
class=
"alert alert-danger py-1 px-2 mb-2 d-none small"
></div>
<div
id=
"time_entries_table_host"
class=
"flex-grow-1 min-h-0 overflow-auto"
></div>
<div
id=
"time_entries_table_host"
class=
"flex-grow-1 min-h-0 overflow-auto"
></div>
</div>
</div>
emT3Web/View.TimeEntries.pas
View file @
29263ec6
...
@@ -24,6 +24,7 @@ type
...
@@ -24,6 +24,7 @@ type
xdwdsTimeEntriessummary
:
TStringField
;
xdwdsTimeEntriessummary
:
TStringField
;
xdwdsTimeEntriescategoryDesc
:
TStringField
;
xdwdsTimeEntriescategoryDesc
:
TStringField
;
btnAddEntry
:
TWebButton
;
btnAddEntry
:
TWebButton
;
lblValidationMessage
:
TWebLabel
;
procedure
edtWeekOfChange
(
Sender
:
TObject
);
procedure
edtWeekOfChange
(
Sender
:
TObject
);
procedure
edtStartDateChange
(
Sender
:
TObject
);
procedure
edtStartDateChange
(
Sender
:
TObject
);
procedure
edtEndDateChange
(
Sender
:
TObject
);
procedure
edtEndDateChange
(
Sender
:
TObject
);
...
@@ -39,6 +40,10 @@ type
...
@@ -39,6 +40,10 @@ type
FUpdatingDates
:
Boolean
;
FUpdatingDates
:
Boolean
;
FPendingScrollTop
:
Integer
;
FPendingScrollTop
:
Integer
;
FPendingScrollLeft
:
Integer
;
FPendingScrollLeft
:
Integer
;
FActiveRowIndex
:
Integer
;
FPendingEntryId
:
string
;
FLastMouseDownRowIndex
:
Integer
;
FLastMouseDownTime
:
Double
;
[
async
]
procedure
LoadTimeEntries
;
[
async
]
procedure
LoadTimeEntries
;
[
async
]
function
AddTimeEntry
:
Boolean
;
[
async
]
function
AddTimeEntry
:
Boolean
;
procedure
RenderTable
;
procedure
RenderTable
;
...
@@ -55,6 +60,19 @@ type
...
@@ -55,6 +60,19 @@ type
procedure
SetTimeEntriesLabel
(
const
AName
:
string
);
procedure
SetTimeEntriesLabel
(
const
AName
:
string
);
procedure
CaptureTableScroll
;
procedure
CaptureTableScroll
;
procedure
RestoreTableScroll
;
procedure
RestoreTableScroll
;
procedure
EditorInput
(
Event
:
TJSEvent
);
procedure
RowFocusOut
(
Event
:
TJSEvent
);
procedure
RowClick
(
Event
:
TJSEvent
);
function
ValidateRow
(
AIndex
:
Integer
):
Boolean
;
procedure
ApplyPendingEntryFocus
;
procedure
ApplyRowValidation
(
AIndex
:
Integer
);
procedure
ClearRowValidation
(
AIndex
:
Integer
);
procedure
ShowRowValidationMessage
(
AIndex
:
Integer
;
const
AMessage
:
string
);
procedure
HideRowValidationMessage
;
[
async
]
procedure
SaveRow
(
AIndex
:
Integer
);
procedure
SetTopControlsEnabled
(
AEnabled
:
Boolean
);
function
GetTargetRowIndex
(
ATarget
:
TJSHTMLElement
):
Integer
;
procedure
DocumentMouseDown
(
Event
:
TJSEvent
);
public
public
end
;
end
;
...
@@ -81,6 +99,12 @@ begin
...
@@ -81,6 +99,12 @@ begin
FUpdatingDates
:=
False
;
FUpdatingDates
:=
False
;
FPendingScrollTop
:=
0
;
FPendingScrollTop
:=
0
;
FPendingScrollLeft
:=
0
;
FPendingScrollLeft
:=
0
;
FActiveRowIndex
:=
-
1
;
FPendingEntryId
:=
''
;
FLastMouseDownRowIndex
:=
-
1
;
FLastMouseDownTime
:=
0
;
document
.
addEventListener
(
'mousedown'
,
TJSEventHandler
(@
DocumentMouseDown
));
payload
:=
AuthService
.
TokenPayload
;
payload
:=
AuthService
.
TokenPayload
;
if
Assigned
(
payload
)
then
if
Assigned
(
payload
)
then
...
@@ -113,6 +137,7 @@ begin
...
@@ -113,6 +137,7 @@ begin
LoadTimeEntries
;
LoadTimeEntries
;
end
;
end
;
function
TFTimeEntries
.
HtmlEncode
(
const
s
:
string
):
string
;
function
TFTimeEntries
.
HtmlEncode
(
const
s
:
string
):
string
;
begin
begin
Result
:=
s
;
Result
:=
s
;
...
@@ -123,6 +148,7 @@ begin
...
@@ -123,6 +148,7 @@ begin
Result
:=
StringReplace
(
Result
,
''''
,
'''
,
[
rfReplaceAll
]);
Result
:=
StringReplace
(
Result
,
''''
,
'''
,
[
rfReplaceAll
]);
end
;
end
;
function
TFTimeEntries
.
IsoToDate
(
const
AValue
:
string
):
TDateTime
;
function
TFTimeEntries
.
IsoToDate
(
const
AValue
:
string
):
TDateTime
;
var
var
yearValue
:
Integer
;
yearValue
:
Integer
;
...
@@ -217,7 +243,6 @@ var
...
@@ -217,7 +243,6 @@ var
el
:
TJSHTMLElement
;
el
:
TJSHTMLElement
;
begin
begin
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_total_rows'
));
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_total_rows'
));
if
Assigned
(
el
)
then
el
.
innerText
:=
'Total Rows: '
+
IntToStr
(
ARowCount
);
el
.
innerText
:=
'Total Rows: '
+
IntToStr
(
ARowCount
);
end
;
end
;
...
@@ -231,7 +256,6 @@ begin
...
@@ -231,7 +256,6 @@ begin
displayName
:=
'User'
;
displayName
:=
'User'
;
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_entries_title'
));
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_entries_title'
));
if
Assigned
(
el
)
then
el
.
innerText
:=
displayName
+
'''s Time Entries'
;
el
.
innerText
:=
displayName
+
'''s Time Entries'
;
end
;
end
;
...
@@ -265,41 +289,29 @@ begin
...
@@ -265,41 +289,29 @@ begin
end
;
end
;
end
;
end
;
if
not
Assigned
(
response
.
Result
)
then
Exit
;
resultObj
:=
TJSObject
(
response
.
Result
);
resultObj
:=
TJSObject
(
response
.
Result
);
if
resultObj
.
hasOwnProperty
(
'userName'
)
then
begin
userNameValue
:=
string
(
resultObj
[
'userName'
]);
userNameValue
:=
string
(
resultObj
[
'userName'
]);
if
userNameValue
<>
''
then
if
userNameValue
<>
''
then
SetTimeEntriesLabel
(
userNameValue
);
SetTimeEntriesLabel
(
userNameValue
)
end
else
else
SetTimeEntriesLabel
(
FUserName
);
SetTimeEntriesLabel
(
FUserName
);
rowCount
:=
StrToIntDef
(
string
(
resultObj
[
'count'
]),
0
);
rowCount
:=
StrToIntDef
(
string
(
resultObj
[
'count'
]),
0
);
SetTotalRowsLabel
(
rowCount
);
SetTotalRowsLabel
(
rowCount
);
if
resultObj
.
hasOwnProperty
(
'taskOptions'
)
then
FTaskOptions
:=
TJSArray
(
resultObj
[
'taskOptions'
]);
FTaskOptions
:=
TJSArray
(
resultObj
[
'taskOptions'
])
FCategoryOptions
:=
TJSArray
(
resultObj
[
'categoryOptions'
]);
else
FTaskOptions
:=
TJSArray
.
new
;
if
resultObj
.
hasOwnProperty
(
'categoryOptions'
)
then
FCategoryOptions
:=
TJSArray
(
resultObj
[
'categoryOptions'
])
else
FCategoryOptions
:=
TJSArray
.
new
;
itemsArray
:=
TJSArray
(
resultObj
[
'items'
]);
itemsArray
:=
TJSArray
(
resultObj
[
'items'
]);
if
not
Assigned
(
itemsArray
)
then
itemsArray
:=
TJSArray
.
new
;
xdwdsTimeEntries
.
Close
;
xdwdsTimeEntries
.
Close
;
xdwdsTimeEntries
.
SetJsonData
(
itemsArray
);
xdwdsTimeEntries
.
SetJsonData
(
itemsArray
);
xdwdsTimeEntries
.
Open
;
xdwdsTimeEntries
.
Open
;
FActiveRowIndex
:=
-
1
;
HideRowValidationMessage
;
SetTopControlsEnabled
(
True
);
RenderTable
;
RenderTable
;
finally
finally
Utils
.
HideSpinner
(
'spinner'
);
Utils
.
HideSpinner
(
'spinner'
);
...
@@ -331,6 +343,8 @@ begin
...
@@ -331,6 +343,8 @@ begin
[
FUserId
,
edtWeekOf
.
Text
]
[
FUserId
,
edtWeekOf
.
Text
]
));
));
FPendingEntryId
:=
JS
.
toString
(
response
.
Result
);
console
.
log
(
'AddTimeEntry response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
console
.
log
(
'AddTimeEntry response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
Result
:=
True
;
Result
:=
True
;
except
except
...
@@ -368,7 +382,7 @@ var
...
@@ -368,7 +382,7 @@ var
function
TextInput
(
const
FieldName
,
Value
:
string
;
const
AIdx
:
Integer
):
string
;
function
TextInput
(
const
FieldName
,
Value
:
string
;
const
AIdx
:
Integer
):
string
;
begin
begin
Result
:=
Result
:=
'<input class="form-control form-control-sm cell-input time-editor w-100"
readonly
'
+
'<input class="form-control form-control-sm cell-input time-editor w-100" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'value="'
+
HtmlEncode
(
Value
)
+
'">'
;
'value="'
+
HtmlEncode
(
Value
)
+
'">'
;
end
;
end
;
...
@@ -380,7 +394,7 @@ var
...
@@ -380,7 +394,7 @@ var
dateValue
:=
Utils
.
NormalizeDateValue
(
Value
);
dateValue
:=
Utils
.
NormalizeDateValue
(
Value
);
Result
:=
Result
:=
'<input type="date" class="form-control form-control-sm cell-input time-editor w-100"
readonly
'
+
'<input type="date" class="form-control form-control-sm cell-input time-editor w-100" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'value="'
+
HtmlEncode
(
dateValue
)
+
'">'
;
'value="'
+
HtmlEncode
(
dateValue
)
+
'">'
;
end
;
end
;
...
@@ -421,7 +435,7 @@ var
...
@@ -421,7 +435,7 @@ var
'data-display="" '
+
'data-display="" '
+
'data-trigger-id="'
+
triggerId
+
'"></button>'
;
'data-trigger-id="'
+
triggerId
+
'"></button>'
;
if
Assigned
(
Items
)
then
for
i
:=
0
to
Items
.
length
-
1
do
for
i
:=
0
to
Items
.
length
-
1
do
begin
begin
optionObj
:=
TJSObject
(
Items
[
i
]);
optionObj
:=
TJSObject
(
Items
[
i
]);
...
@@ -445,15 +459,13 @@ var
...
@@ -445,15 +459,13 @@ var
function
SummaryTextArea
(
const
FieldName
,
Value
:
string
;
const
AIdx
:
Integer
):
string
;
function
SummaryTextArea
(
const
FieldName
,
Value
:
string
;
const
AIdx
:
Integer
):
string
;
begin
begin
Result
:=
Result
:=
'<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100"
readonly
'
+
'<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'style="min-height:31px; height:31px; overflow:hidden; resize:none;">'
+
HtmlEncode
(
Value
)
+
'</textarea>'
;
'style="min-height:31px; height:31px; overflow:hidden; resize:none;">'
+
HtmlEncode
(
Value
)
+
'</textarea>'
;
end
;
end
;
begin
begin
host
:=
TJSHTMLElement
(
document
.
getElementById
(
'time_entries_table_host'
));
host
:=
TJSHTMLElement
(
document
.
getElementById
(
'time_entries_table_host'
));
if
not
Assigned
(
host
)
then
Exit
;
html
:=
html
:=
'<div class="time-vscroll">'
+
'<div class="time-vscroll">'
+
...
@@ -486,7 +498,7 @@ begin
...
@@ -486,7 +498,7 @@ begin
hoursText
:=
FormatFloat
(
'0.##'
,
xdwdsTimeEntrieshours
.
AsFloat
);
hoursText
:=
FormatFloat
(
'0.##'
,
xdwdsTimeEntrieshours
.
AsFloat
);
html
:=
html
+
html
:=
html
+
'<tr
data-entry-id="'
+
IntToStr
(
xdwdsTimeEntriesentryId
.
AsInteger
)
+
'" data-task-id="'
+
xdwdsTimeEntriestaskId
.
AsString
+
'">'
+
'<tr class="time-row-selectable" data-idx="'
+
IntToStr
(
rowIdx
)
+
'"
data-entry-id="'
+
IntToStr
(
xdwdsTimeEntriesentryId
.
AsInteger
)
+
'" data-task-id="'
+
xdwdsTimeEntriestaskId
.
AsString
+
'">'
+
TdNowrap
(
DateInput
(
'taskDate'
,
xdwdsTimeEntriestaskDate
.
AsString
,
rowIdx
))
+
TdNowrap
(
DateInput
(
'taskDate'
,
xdwdsTimeEntriestaskDate
.
AsString
,
rowIdx
))
+
TdNowrap
(
SelectList
(
'taskId'
,
xdwdsTimeEntriestaskId
.
AsString
,
xdwdsTimeEntriestaskDisplay
.
AsString
,
rowIdx
,
FTaskOptions
,
'taskId'
,
'taskDisplay'
))
+
TdNowrap
(
SelectList
(
'taskId'
,
xdwdsTimeEntriestaskId
.
AsString
,
xdwdsTimeEntriestaskDisplay
.
AsString
,
rowIdx
,
FTaskOptions
,
'taskId'
,
'taskDisplay'
))
+
TdNowrap
(
HoursInput
(
'hours'
,
hoursText
,
rowIdx
))
+
TdNowrap
(
HoursInput
(
'hours'
,
hoursText
,
rowIdx
))
+
...
@@ -506,6 +518,7 @@ begin
...
@@ -506,6 +518,7 @@ begin
EnableAutoGrowTextAreas
;
EnableAutoGrowTextAreas
;
EnableColumnResize
;
EnableColumnResize
;
RestoreTableScroll
;
RestoreTableScroll
;
ApplyPendingEntryFocus
;
end
;
end
;
procedure
TFTimeEntries
.
BindTableEditors
;
procedure
TFTimeEntries
.
BindTableEditors
;
...
@@ -518,15 +531,202 @@ begin
...
@@ -518,15 +531,202 @@ begin
nodes
:=
document
.
querySelectorAll
(
'.time-editor'
);
nodes
:=
document
.
querySelectorAll
(
'.time-editor'
);
console
.
log
(
'BindTableEditors: time-editor count='
+
IntToStr
(
nodes
.
length
));
console
.
log
(
'BindTableEditors: time-editor count='
+
IntToStr
(
nodes
.
length
));
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
addEventListener
(
'input'
,
TJSEventHandler
(@
EditorInput
));
end
;
nodes
:=
document
.
querySelectorAll
(
'.time-dd-item'
);
nodes
:=
document
.
querySelectorAll
(
'.time-dd-item'
);
console
.
log
(
'BindTableEditors: time-dd-item count='
+
IntToStr
(
nodes
.
length
));
console
.
log
(
'BindTableEditors: time-dd-item count='
+
IntToStr
(
nodes
.
length
));
for
i
:=
0
to
nodes
.
length
-
1
do
for
i
:=
0
to
nodes
.
length
-
1
do
begin
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
DropdownItemClick
));
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
DropdownItemClick
));
end
;
end
;
nodes
:=
document
.
querySelectorAll
(
'.time-row-selectable'
);
console
.
log
(
'BindTableEditors: time-row-selectable count='
+
IntToStr
(
nodes
.
length
));
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
RowClick
));
el
.
addEventListener
(
'focusout'
,
TJSEventHandler
(@
RowFocusOut
));
end
;
end
;
procedure
TFTimeEntries
.
EditorInput
(
Event
:
TJSEvent
);
var
el
:
TJSHTMLElement
;
idx
:
Integer
;
idxStr
:
string
;
fieldName
:
string
;
newVal
:
string
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
el
:=
TJSHTMLElement
(
Event
.
target
);
idxStr
:=
string
(
el
.
getAttribute
(
'data-idx'
));
fieldName
:=
string
(
el
.
getAttribute
(
'data-field'
));
idx
:=
StrToIntDef
(
idxStr
,
-
1
);
if
(
idx
<
0
)
or
(
fieldName
=
''
)
then
Exit
;
FActiveRowIndex
:=
idx
;
GotoRowIndex
(
idx
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
newVal
:=
string
(
TJSObject
(
el
)[
'value'
]);
xdwdsTimeEntries
.
Edit
;
if
SameText
(
fieldName
,
'taskDate'
)
then
xdwdsTimeEntriestaskDate
.
AsString
:=
newVal
else
if
SameText
(
fieldName
,
'hours'
)
then
begin
if
Trim
(
newVal
)
=
''
then
xdwdsTimeEntrieshours
.
Clear
else
xdwdsTimeEntrieshours
.
AsFloat
:=
StrToFloatDef
(
newVal
,
0
);
end
else
if
SameText
(
fieldName
,
'taskTime'
)
then
xdwdsTimeEntriestaskTime
.
AsString
:=
newVal
else
if
SameText
(
fieldName
,
'summary'
)
then
xdwdsTimeEntriessummary
.
AsString
:=
newVal
else
xdwdsTimeEntries
.
FieldByName
(
fieldName
).
AsString
:=
newVal
;
xdwdsTimeEntries
.
Post
;
el
.
setAttribute
(
'data-unsaved-data'
,
'1'
);
if
ValidateRow
(
idx
)
then
begin
ClearRowValidation
(
idx
);
SetTopControlsEnabled
(
True
);
end
else
SetTopControlsEnabled
(
False
);
end
;
[
async
]
procedure
TFTimeEntries
.
SaveRow
(
AIndex
:
Integer
);
var
saveObj
:
TJSObject
;
response
:
TXDataClientResponse
;
rowEl
:
TJSHTMLElement
;
nodes
:
TJSNodeList
;
i
:
Integer
;
el
:
TJSHTMLElement
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
AIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
if
not
ValidateRow
(
AIndex
)
then
begin
ApplyRowValidation
(
AIndex
);
ShowRowValidationMessage
(
AIndex
,
'Complete required fields before leaving this row.'
);
Exit
;
end
;
saveObj
:=
TJSObject
.
new
;
saveObj
[
'entryId'
]
:=
xdwdsTimeEntriesentryId
.
AsString
;
saveObj
[
'userId'
]
:=
FUserId
;
saveObj
[
'taskDate'
]
:=
xdwdsTimeEntriestaskDate
.
AsString
;
saveObj
[
'taskId'
]
:=
xdwdsTimeEntriestaskId
.
AsString
;
saveObj
[
'hours'
]
:=
xdwdsTimeEntrieshours
.
AsFloat
;
saveObj
[
'taskTime'
]
:=
xdwdsTimeEntriestaskTime
.
AsString
;
saveObj
[
'category'
]
:=
xdwdsTimeEntriescategory
.
AsString
;
saveObj
[
'summary'
]
:=
xdwdsTimeEntriessummary
.
AsString
;
try
response
:=
await
(
xdwcTimeEntries
.
RawInvokeAsync
(
'ITimeEntryService.SaveTimeEntry'
,
[
saveObj
]
));
console
.
log
(
'SaveRow response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-idx="'
+
IntToStr
(
AIndex
)
+
'"]'
));
nodes
:=
rowEl
.
querySelectorAll
(
'[data-unsaved-data="1"]'
);
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
removeAttribute
(
'data-unsaved-data'
);
end
;
ClearRowValidation
(
AIndex
);
except
on
E
:
EXDataClientRequestException
do
begin
console
.
log
(
'SaveRow ERROR: '
+
E
.
ErrorResult
.
ErrorMessage
);
Utils
.
ShowErrorModal
(
E
.
ErrorResult
.
ErrorMessage
);
end
;
end
;
end
;
procedure
TFTimeEntries
.
RowClick
(
Event
:
TJSEvent
);
var
rowEl
:
TJSHTMLElement
;
idx
:
Integer
;
begin
rowEl
:=
TJSHTMLElement
(
Event
.
currentTarget
);
idx
:=
StrToIntDef
(
string
(
rowEl
.
getAttribute
(
'data-idx'
)),
-
1
);
if
idx
<
0
then
Exit
;
FActiveRowIndex
:=
idx
;
end
;
procedure
TFTimeEntries
.
RowFocusOut
(
Event
:
TJSEvent
);
var
rowEl
:
TJSHTMLElement
;
idx
:
Integer
;
begin
rowEl
:=
TJSHTMLElement
(
Event
.
currentTarget
);
window
.
setTimeout
(
procedure
begin
idx
:=
StrToIntDef
(
string
(
rowEl
.
getAttribute
(
'data-idx'
)),
-
1
);
if
idx
<
0
then
Exit
;
if
(
FLastMouseDownRowIndex
=
idx
)
and
((
TJSDate
.
now
-
FLastMouseDownTime
)
<
500
)
then
Exit
;
if
Assigned
(
document
.
activeElement
)
and
rowEl
.
contains
(
document
.
activeElement
)
then
Exit
;
if
not
ValidateRow
(
idx
)
then
begin
FActiveRowIndex
:=
idx
;
ApplyRowValidation
(
idx
);
ShowRowValidationMessage
(
idx
,
'Complete required fields before leaving this row.'
);
SetTopControlsEnabled
(
False
);
Exit
;
end
;
ClearRowValidation
(
idx
);
SetTopControlsEnabled
(
True
);
if
Assigned
(
rowEl
.
querySelector
(
'[data-unsaved-data="1"]'
))
then
SaveRow
(
idx
);
end
,
0
);
end
;
end
;
...
@@ -580,16 +780,126 @@ begin
...
@@ -580,16 +780,126 @@ begin
xdwdsTimeEntries
.
Post
;
xdwdsTimeEntries
.
Post
;
if
triggerId
<>
''
then
FActiveRowIndex
:=
idx
;
begin
btn
:=
TJSHTMLElement
(
document
.
getElementById
(
triggerId
));
btn
:=
TJSHTMLElement
(
document
.
getElementById
(
triggerId
));
if
Assigned
(
btn
)
then
btn
.
setAttribute
(
'data-unsaved-data'
,
'1'
);
begin
labelEl
:=
TJSHTMLElement
(
btn
.
querySelector
(
'.time-dd-label'
));
labelEl
:=
TJSHTMLElement
(
btn
.
querySelector
(
'.time-dd-label'
));
if
Assigned
(
labelEl
)
then
labelEl
.
textContent
:=
newDisplay
;
labelEl
.
textContent
:=
newDisplay
;
btn
.
focus
;
if
ValidateRow
(
idx
)
then
begin
ClearRowValidation
(
idx
);
SetTopControlsEnabled
(
True
);
end
else
SetTopControlsEnabled
(
False
);
end
;
function
TFTimeEntries
.
ValidateRow
(
AIndex
:
Integer
):
Boolean
;
var
hasDate
:
Boolean
;
hasTask
:
Boolean
;
hasHours
:
Boolean
;
hasCategory
:
Boolean
;
hasSummary
:
Boolean
;
begin
Result
:=
False
;
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
AIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
hasDate
:=
Trim
(
xdwdsTimeEntriestaskDate
.
AsString
)
<>
''
;
hasTask
:=
Trim
(
xdwdsTimeEntriestaskId
.
AsString
)
<>
''
;
if
xdwdsTimeEntrieshours
.
IsNull
then
hasHours
:=
False
else
hasHours
:=
xdwdsTimeEntrieshours
.
AsFloat
>
0
;
hasCategory
:=
Trim
(
xdwdsTimeEntriescategory
.
AsString
)
<>
''
;
hasSummary
:=
Trim
(
xdwdsTimeEntriessummary
.
AsString
)
<>
''
;
Result
:=
hasDate
and
hasTask
and
hasHours
and
hasCategory
and
hasSummary
;
end
;
procedure
TFTimeEntries
.
ApplyPendingEntryFocus
;
var
rowEl
:
TJSHTMLElement
;
idx
:
Integer
;
firstEditor
:
TJSHTMLElement
;
begin
if
FPendingEntryId
=
''
then
Exit
;
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-entry-id="'
+
FPendingEntryId
+
'"]'
));
if
not
Assigned
(
rowEl
)
then
begin
FPendingEntryId
:=
''
;
Exit
;
end
;
end
;
idx
:=
StrToIntDef
(
string
(
rowEl
.
getAttribute
(
'data-idx'
)),
-
1
);
if
idx
<
0
then
begin
FPendingEntryId
:=
''
;
Exit
;
end
;
end
;
FActiveRowIndex
:=
idx
;
SetTopControlsEnabled
(
False
);
firstEditor
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'[data-field="taskDate"]'
));
if
Assigned
(
firstEditor
)
then
firstEditor
.
focus
;
FPendingEntryId
:=
''
;
end
;
procedure
TFTimeEntries
.
ApplyRowValidation
(
AIndex
:
Integer
);
var
rowEl
:
TJSHTMLElement
;
hoursInvalid
:
Boolean
;
procedure
SetInvalid
(
const
AFieldName
:
string
;
AInvalid
:
Boolean
);
var
fieldEl
:
TJSHTMLElement
;
begin
fieldEl
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'[data-field="'
+
AFieldName
+
'"]'
));
if
AInvalid
then
fieldEl
.
classList
.
add
(
'is-invalid'
)
else
fieldEl
.
classList
.
remove
(
'is-invalid'
);
end
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
AIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-idx="'
+
IntToStr
(
AIndex
)
+
'"]'
));
if
xdwdsTimeEntrieshours
.
IsNull
then
hoursInvalid
:=
True
else
hoursInvalid
:=
xdwdsTimeEntrieshours
.
AsFloat
<=
0
;
SetInvalid
(
'taskDate'
,
Trim
(
xdwdsTimeEntriestaskDate
.
AsString
)
=
''
);
SetInvalid
(
'taskId'
,
Trim
(
xdwdsTimeEntriestaskId
.
AsString
)
=
''
);
SetInvalid
(
'hours'
,
hoursInvalid
);
SetInvalid
(
'category'
,
Trim
(
xdwdsTimeEntriescategory
.
AsString
)
=
''
);
SetInvalid
(
'summary'
,
Trim
(
xdwdsTimeEntriessummary
.
AsString
)
=
''
);
end
;
end
;
...
@@ -605,7 +915,6 @@ begin
...
@@ -605,7 +915,6 @@ begin
editor.style.height = 'auto';
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
editor.style.height = editor.scrollHeight + 'px';
}
;
}
;
fit
();
fit
();
editor
.
addEventListener
(
'input'
,
fit
);
editor
.
addEventListener
(
'input'
,
fit
);
});
});
...
@@ -613,6 +922,79 @@ begin
...
@@ -613,6 +922,79 @@ begin
end
;
end
;
end
;
end
;
procedure
TFTimeEntries
.
ClearRowValidation
(
AIndex
:
Integer
);
var
rowEl
:
TJSHTMLElement
;
nodes
:
TJSNodeList
;
i
:
Integer
;
el
:
TJSHTMLElement
;
begin
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-idx="'
+
IntToStr
(
AIndex
)
+
'"]'
));
nodes
:=
rowEl
.
querySelectorAll
(
'.is-invalid'
);
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
classList
.
remove
(
'is-invalid'
);
end
;
SetTopControlsEnabled
(
True
);
HideRowValidationMessage
;
end
;
procedure
TFTimeEntries
.
ShowRowValidationMessage
(
AIndex
:
Integer
;
const
AMessage
:
string
);
var
rowEl
:
TJSHTMLElement
;
firstInvalidEl
:
TJSHTMLElement
;
begin
lblValidationMessage
.
Caption
:=
AMessage
;
lblValidationMessage
.
ElementHandle
.
classList
.
remove
(
'd-none'
);
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-idx="'
+
IntToStr
(
AIndex
)
+
'"]'
));
firstInvalidEl
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'.is-invalid'
));
firstInvalidEl
.
focus
;
end
;
procedure
TFTimeEntries
.
HideRowValidationMessage
;
begin
lblValidationMessage
.
Caption
:=
''
;
lblValidationMessage
.
ElementHandle
.
classList
.
add
(
'd-none'
);
end
;
function
TFTimeEntries
.
GetTargetRowIndex
(
ATarget
:
TJSHTMLElement
):
Integer
;
var
el
:
TJSHTMLElement
;
idxText
:
string
;
begin
Result
:=
-
1
;
el
:=
ATarget
;
while
Assigned
(
el
)
do
begin
idxText
:=
string
(
el
.
getAttribute
(
'data-idx'
));
if
idxText
<>
''
then
begin
Result
:=
StrToIntDef
(
idxText
,
-
1
);
if
Result
>=
0
then
Exit
;
end
;
el
:=
TJSHTMLElement
(
el
.
parentElement
);
end
;
end
;
procedure
TFTimeEntries
.
DocumentMouseDown
(
Event
:
TJSEvent
);
begin
FLastMouseDownRowIndex
:=
GetTargetRowIndex
(
TJSHTMLElement
(
Event
.
target
));
FLastMouseDownTime
:=
TJSDate
.
now
;
end
;
procedure
TFTimeEntries
.
EnableColumnResize
;
procedure
TFTimeEntries
.
EnableColumnResize
;
begin
begin
asm
asm
...
@@ -665,6 +1047,7 @@ begin
...
@@ -665,6 +1047,7 @@ begin
end
;
end
;
end
;
end
;
[
async
]
procedure
TFTimeEntries
.
btnAddEntryClick
(
Sender
:
TObject
);
[
async
]
procedure
TFTimeEntries
.
btnAddEntryClick
(
Sender
:
TObject
);
begin
begin
Utils
.
ShowSpinner
(
'spinner'
);
Utils
.
ShowSpinner
(
'spinner'
);
...
@@ -679,6 +1062,7 @@ begin
...
@@ -679,6 +1062,7 @@ begin
end
;
end
;
end
;
end
;
procedure
TFTimeEntries
.
CaptureTableScroll
;
procedure
TFTimeEntries
.
CaptureTableScroll
;
begin
begin
asm
asm
...
@@ -690,6 +1074,7 @@ begin
...
@@ -690,6 +1074,7 @@ begin
end
;
end
;
end
;
end
;
procedure
TFTimeEntries
.
RestoreTableScroll
;
procedure
TFTimeEntries
.
RestoreTableScroll
;
begin
begin
asm
asm
...
@@ -701,4 +1086,12 @@ begin
...
@@ -701,4 +1086,12 @@ begin
end
;
end
;
end
;
end
;
procedure
TFTimeEntries
.
SetTopControlsEnabled
(
AEnabled
:
Boolean
);
begin
edtWeekOf
.
Enabled
:=
AEnabled
;
edtStartDate
.
Enabled
:=
AEnabled
;
edtEndDate
.
Enabled
:=
AEnabled
;
btnAddEntry
.
Enabled
:=
AEnabled
;
end
;
end
.
end
.
emT3Web/css/time-entries.css
View file @
29263ec6
...
@@ -45,3 +45,8 @@
...
@@ -45,3 +45,8 @@
.time-table
.dropdown-menu
{
.time-table
.dropdown-menu
{
z-index
:
1055
;
z-index
:
1055
;
}
}
.time-dd-toggle.is-invalid
{
border-color
:
var
(
--bs-danger
)
!important
;
padding-right
:
calc
(
1.5em
+
.75rem
);
}
\ No newline at end of file
emT3XDataServer/Source/Api.Database.dfm
View file @
29263ec6
...
@@ -1142,4 +1142,68 @@ object ApiDatabase: TApiDatabase
...
@@ -1142,4 +1142,68 @@ object ApiDatabase: TApiDatabase
Value = nil
Value = nil
end>
end>
end
end
object uqSaveTimeEntry: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'UPDATE time_items'
'SET'
' TASK_DATE = :TASK_DATE,'
' TASK_ID = :TASK_ID,'
' HOURS = :HOURS,'
' TASK_TIME = :TASK_TIME,'
' CATEGORY = :CATEGORY,'
' SUMMARY = :SUMMARY,'
' MODIFY_DATE = now(),'
' MODIFIED_BY = :MODIFIED_BY'
'WHERE ENTRY_ID = :ENTRY_ID'
' AND USER_ID = :USER_ID')
Left = 544
Top = 424
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_DATE'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'HOURS'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_TIME'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY'
Value = nil
end
item
DataType = ftUnknown
Name = 'SUMMARY'
Value = nil
end
item
DataType = ftUnknown
Name = 'MODIFIED_BY'
Value = nil
end
item
DataType = ftUnknown
Name = 'ENTRY_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
end
end
end
emT3XDataServer/Source/Api.Database.pas
View file @
29263ec6
...
@@ -89,6 +89,7 @@ type
...
@@ -89,6 +89,7 @@ type
uqProjectReportedUsersTASK_ITEM_USER_ID
:
TStringField
;
uqProjectReportedUsersTASK_ITEM_USER_ID
:
TStringField
;
uqProjectReportedUsersNAME
:
TStringField
;
uqProjectReportedUsersNAME
:
TStringField
;
uqAddTimeEntry
:
TUniQuery
;
uqAddTimeEntry
:
TUniQuery
;
uqSaveTimeEntry
:
TUniQuery
;
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
uqUsersCalcFields
(
DataSet
:
TDataSet
);
procedure
uqUsersCalcFields
(
DataSet
:
TDataSet
);
private
private
...
...
emT3XDataServer/Source/TimeEntry.Service.pas
View file @
29263ec6
...
@@ -48,11 +48,24 @@ type
...
@@ -48,11 +48,24 @@ type
destructor
Destroy
;
override
;
destructor
Destroy
;
override
;
end
;
end
;
TTimeEntrySave
=
class
public
entryId
:
string
;
userId
:
string
;
taskDate
:
string
;
taskId
:
string
;
hours
:
Double
;
taskTime
:
string
;
category
:
string
;
summary
:
string
;
end
;
[
ServiceContract
,
Model
(
API_MODEL
)]
[
ServiceContract
,
Model
(
API_MODEL
)]
ITimeEntryService
=
interface
(
IInvokable
)
ITimeEntryService
=
interface
(
IInvokable
)
[
'{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'
]
[
'{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'
]
[
HttpGet
]
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
[
HttpGet
]
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
[
HttpPost
]
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
[
HttpPost
]
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
[
HttpPost
]
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
end
;
end
;
implementation
implementation
...
...
emT3XDataServer/Source/TimeEntry.ServiceImpl.pas
View file @
29263ec6
...
@@ -31,6 +31,7 @@ type
...
@@ -31,6 +31,7 @@ type
public
public
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
end
;
end
;
implementation
implementation
...
@@ -429,6 +430,38 @@ begin
...
@@ -429,6 +430,38 @@ begin
end
;
end
;
function
TTimeEntryService
.
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
var
taskDateValue
:
TDateTime
;
begin
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"'
,
[
Item
.
entryId
,
Item
.
userId
]));
taskDateValue
:=
ParseIsoDate
(
Item
.
taskDate
);
apiDB
.
uqSaveTimeEntry
.
Close
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'ENTRY_ID'
).
AsString
:=
Item
.
entryId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'USER_ID'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_DATE'
).
AsDateTime
:=
taskDateValue
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_ID'
).
AsString
:=
Item
.
taskId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'HOURS'
).
AsFloat
:=
Item
.
hours
;
if
Trim
(
Item
.
taskTime
)
=
''
then
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_TIME'
).
Clear
else
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_TIME'
).
AsString
:=
Item
.
taskTime
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'CATEGORY'
).
AsString
:=
Item
.
category
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'SUMMARY'
).
AsString
:=
Item
.
summary
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'MODIFIED_BY'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqSaveTimeEntry
.
ExecSQL
;
Result
:=
True
;
Logger
.
Log
(
4
,
'TimeEntryService.SaveTimeEntry - saved ENTRY_ID='
+
Item
.
entryId
);
end
;
initialization
initialization
RegisterServiceType
(
TTimeEntryService
);
RegisterServiceType
(
TTimeEntryService
);
...
...
emT3XDataServer/bin/emT3XDataServer.ini
View file @
29263ec6
...
@@ -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
=
2
19
LogFileNum
=
2
22
[Database]
[Database]
Server
=
192.168.102.131
Server
=
192.168.102.131
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment