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
Height = 480
ElementFont = efCSS
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
Left = 71
Top = 80
...
...
emT3Web/View.TimeEntries.html
View file @
29263ec6
...
...
@@ -23,5 +23,6 @@
<button
id=
"btn_add_entry"
class=
"btn btn-sm btn-success text-nowrap"
>
Add Entry
</button>
</div>
</div>
<div
id=
"lbl_validation_message"
class=
"alert alert-danger py-1 px-2 mb-2 d-none small"
></div>
<div
id=
"time_entries_table_host"
class=
"flex-grow-1 min-h-0 overflow-auto"
></div>
</div>
emT3Web/View.TimeEntries.pas
View file @
29263ec6
...
...
@@ -24,6 +24,7 @@ type
xdwdsTimeEntriessummary
:
TStringField
;
xdwdsTimeEntriescategoryDesc
:
TStringField
;
btnAddEntry
:
TWebButton
;
lblValidationMessage
:
TWebLabel
;
procedure
edtWeekOfChange
(
Sender
:
TObject
);
procedure
edtStartDateChange
(
Sender
:
TObject
);
procedure
edtEndDateChange
(
Sender
:
TObject
);
...
...
@@ -39,6 +40,10 @@ type
FUpdatingDates
:
Boolean
;
FPendingScrollTop
:
Integer
;
FPendingScrollLeft
:
Integer
;
FActiveRowIndex
:
Integer
;
FPendingEntryId
:
string
;
FLastMouseDownRowIndex
:
Integer
;
FLastMouseDownTime
:
Double
;
[
async
]
procedure
LoadTimeEntries
;
[
async
]
function
AddTimeEntry
:
Boolean
;
procedure
RenderTable
;
...
...
@@ -55,6 +60,19 @@ type
procedure
SetTimeEntriesLabel
(
const
AName
:
string
);
procedure
CaptureTableScroll
;
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
end
;
...
...
@@ -81,6 +99,12 @@ begin
FUpdatingDates
:=
False
;
FPendingScrollTop
:=
0
;
FPendingScrollLeft
:=
0
;
FActiveRowIndex
:=
-
1
;
FPendingEntryId
:=
''
;
FLastMouseDownRowIndex
:=
-
1
;
FLastMouseDownTime
:=
0
;
document
.
addEventListener
(
'mousedown'
,
TJSEventHandler
(@
DocumentMouseDown
));
payload
:=
AuthService
.
TokenPayload
;
if
Assigned
(
payload
)
then
...
...
@@ -113,6 +137,7 @@ begin
LoadTimeEntries
;
end
;
function
TFTimeEntries
.
HtmlEncode
(
const
s
:
string
):
string
;
begin
Result
:=
s
;
...
...
@@ -123,6 +148,7 @@ begin
Result
:=
StringReplace
(
Result
,
''''
,
'''
,
[
rfReplaceAll
]);
end
;
function
TFTimeEntries
.
IsoToDate
(
const
AValue
:
string
):
TDateTime
;
var
yearValue
:
Integer
;
...
...
@@ -217,7 +243,6 @@ var
el
:
TJSHTMLElement
;
begin
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_total_rows'
));
if
Assigned
(
el
)
then
el
.
innerText
:=
'Total Rows: '
+
IntToStr
(
ARowCount
);
end
;
...
...
@@ -231,7 +256,6 @@ begin
displayName
:=
'User'
;
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'lbl_time_entries_title'
));
if
Assigned
(
el
)
then
el
.
innerText
:=
displayName
+
'''s Time Entries'
;
end
;
...
...
@@ -265,41 +289,29 @@ begin
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
SetTimeEntriesLabel
(
userNameValue
)
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
;
FTaskOptions
:=
TJSArray
(
resultObj
[
'taskOptions'
]);
FCategoryOptions
:=
TJSArray
(
resultObj
[
'categoryOptions'
]);
itemsArray
:=
TJSArray
(
resultObj
[
'items'
]);
if
not
Assigned
(
itemsArray
)
then
itemsArray
:=
TJSArray
.
new
;
xdwdsTimeEntries
.
Close
;
xdwdsTimeEntries
.
SetJsonData
(
itemsArray
);
xdwdsTimeEntries
.
Open
;
FActiveRowIndex
:=
-
1
;
HideRowValidationMessage
;
SetTopControlsEnabled
(
True
);
RenderTable
;
finally
Utils
.
HideSpinner
(
'spinner'
);
...
...
@@ -331,6 +343,8 @@ begin
[
FUserId
,
edtWeekOf
.
Text
]
));
FPendingEntryId
:=
JS
.
toString
(
response
.
Result
);
console
.
log
(
'AddTimeEntry response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
Result
:=
True
;
except
...
...
@@ -368,7 +382,7 @@ var
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
'
+
'<input class="form-control form-control-sm cell-input time-editor w-100" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'" '
+
'value="'
+
HtmlEncode
(
Value
)
+
'">'
;
end
;
...
...
@@ -380,7 +394,7 @@ var
dateValue
:=
Utils
.
NormalizeDateValue
(
Value
);
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
+
'" '
+
'value="'
+
HtmlEncode
(
dateValue
)
+
'">'
;
end
;
...
...
@@ -421,7 +435,7 @@ var
'data-display="" '
+
'data-trigger-id="'
+
triggerId
+
'"></button>'
;
if
Assigned
(
Items
)
then
for
i
:=
0
to
Items
.
length
-
1
do
begin
optionObj
:=
TJSObject
(
Items
[
i
]);
...
...
@@ -445,15 +459,13 @@ var
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
'
+
'<textarea rows="1" class="form-control form-control-sm cell-textarea time-textarea time-editor w-100" '
+
'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">'
+
...
...
@@ -486,7 +498,7 @@ begin
hoursText
:=
FormatFloat
(
'0.##'
,
xdwdsTimeEntrieshours
.
AsFloat
);
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
(
SelectList
(
'taskId'
,
xdwdsTimeEntriestaskId
.
AsString
,
xdwdsTimeEntriestaskDisplay
.
AsString
,
rowIdx
,
FTaskOptions
,
'taskId'
,
'taskDisplay'
))
+
TdNowrap
(
HoursInput
(
'hours'
,
hoursText
,
rowIdx
))
+
...
...
@@ -506,6 +518,7 @@ begin
EnableAutoGrowTextAreas
;
EnableColumnResize
;
RestoreTableScroll
;
ApplyPendingEntryFocus
;
end
;
procedure
TFTimeEntries
.
BindTableEditors
;
...
...
@@ -518,15 +531,202 @@ begin
nodes
:=
document
.
querySelectorAll
(
'.time-editor'
);
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'
);
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
;
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
;
...
...
@@ -580,16 +780,126 @@ begin
xdwdsTimeEntries
.
Post
;
if
triggerId
<>
''
then
begin
FActiveRowIndex
:=
idx
;
btn
:=
TJSHTMLElement
(
document
.
getElementById
(
triggerId
));
if
Assigned
(
btn
)
then
begin
btn
.
setAttribute
(
'data-unsaved-data'
,
'1'
);
labelEl
:=
TJSHTMLElement
(
btn
.
querySelector
(
'.time-dd-label'
));
if
Assigned
(
labelEl
)
then
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
;
idx
:=
StrToIntDef
(
string
(
rowEl
.
getAttribute
(
'data-idx'
)),
-
1
);
if
idx
<
0
then
begin
FPendingEntryId
:=
''
;
Exit
;
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
;
...
...
@@ -605,7 +915,6 @@ begin
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
}
;
fit
();
editor
.
addEventListener
(
'input'
,
fit
);
});
...
...
@@ -613,6 +922,79 @@ begin
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
;
begin
asm
...
...
@@ -665,6 +1047,7 @@ begin
end
;
end
;
[
async
]
procedure
TFTimeEntries
.
btnAddEntryClick
(
Sender
:
TObject
);
begin
Utils
.
ShowSpinner
(
'spinner'
);
...
...
@@ -679,6 +1062,7 @@ begin
end
;
end
;
procedure
TFTimeEntries
.
CaptureTableScroll
;
begin
asm
...
...
@@ -690,6 +1074,7 @@ begin
end
;
end
;
procedure
TFTimeEntries
.
RestoreTableScroll
;
begin
asm
...
...
@@ -701,4 +1086,12 @@ begin
end
;
end
;
procedure
TFTimeEntries
.
SetTopControlsEnabled
(
AEnabled
:
Boolean
);
begin
edtWeekOf
.
Enabled
:=
AEnabled
;
edtStartDate
.
Enabled
:=
AEnabled
;
edtEndDate
.
Enabled
:=
AEnabled
;
btnAddEntry
.
Enabled
:=
AEnabled
;
end
;
end
.
emT3Web/css/time-entries.css
View file @
29263ec6
...
...
@@ -45,3 +45,8 @@
.time-table
.dropdown-menu
{
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
Value = nil
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
emT3XDataServer/Source/Api.Database.pas
View file @
29263ec6
...
...
@@ -89,6 +89,7 @@ type
uqProjectReportedUsersTASK_ITEM_USER_ID
:
TStringField
;
uqProjectReportedUsersNAME
:
TStringField
;
uqAddTimeEntry
:
TUniQuery
;
uqSaveTimeEntry
:
TUniQuery
;
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
uqUsersCalcFields
(
DataSet
:
TDataSet
);
private
...
...
emT3XDataServer/Source/TimeEntry.Service.pas
View file @
29263ec6
...
...
@@ -48,11 +48,24 @@ type
destructor
Destroy
;
override
;
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
)]
ITimeEntryService
=
interface
(
IInvokable
)
[
'{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'
]
[
HttpGet
]
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
[
HttpPost
]
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
[
HttpPost
]
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
end
;
implementation
...
...
emT3XDataServer/Source/TimeEntry.ServiceImpl.pas
View file @
29263ec6
...
...
@@ -31,6 +31,7 @@ type
public
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
end
;
implementation
...
...
@@ -429,6 +430,38 @@ begin
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
RegisterServiceType
(
TTimeEntryService
);
...
...
emT3XDataServer/bin/emT3XDataServer.ini
View file @
29263ec6
...
...
@@ -2,7 +2,7 @@
MemoLogLevel
=
4
FileLogLevel
=
4
webClientVersion
=
0.8.9
LogFileNum
=
2
19
LogFileNum
=
2
22
[Database]
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