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
7a8e3164
Commit
7a8e3164
authored
May 23, 2026
by
Mac Stephens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add field-level time entry saving, focused task dropdowns, place support, and delete entry flow
parent
29263ec6
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1502 additions
and
184 deletions
+1502
-184
Utils.pas
emT3Web/Utils.pas
+6
-0
View.Main.html
emT3Web/View.Main.html
+19
-15
View.TimeEntries.dfm
emT3Web/View.TimeEntries.dfm
+36
-4
View.TimeEntries.html
emT3Web/View.TimeEntries.html
+56
-4
View.TimeEntries.pas
emT3Web/View.TimeEntries.pas
+457
-135
emT3web.dpr
emT3Web/emT3web.dpr
+2
-1
emT3web.dproj
emT3Web/emT3web.dproj
+1
-0
uTaskPickerOffCanvas.pas
emT3Web/uTaskPickerOffCanvas.pas
+334
-0
Api.Database.dfm
emT3XDataServer/Source/Api.Database.dfm
+155
-0
Api.Database.pas
emT3XDataServer/Source/Api.Database.pas
+17
-0
TimeEntry.Service.pas
emT3XDataServer/Source/TimeEntry.Service.pas
+46
-0
TimeEntry.ServiceImpl.pas
emT3XDataServer/Source/TimeEntry.ServiceImpl.pas
+372
-24
emT3XDataServer.ini
emT3XDataServer/bin/emT3XDataServer.ini
+1
-1
No files found.
emT3Web/Utils.pas
View file @
7a8e3164
...
...
@@ -160,6 +160,12 @@ begin
var
btnRight
=
document
.
getElementById
(
'btn_confirm_right'
);
var
bsModal
;
console
.
log
(
'confirmation modal='
,
modal
);
console
.
log
(
'confirmation body='
,
body
);
console
.
log
(
'confirmation left='
,
btnLeft
);
console
.
log
(
'confirmation right='
,
btnRight
);
if
(
body
)
body
.
innerText
=
msg
;
if
(
btnLeft
)
btnLeft
.
innerText
=
leftLabel
;
if
(
btnRight
)
btnRight
.
innerText
=
rightLabel
;
...
...
emT3Web/View.Main.html
View file @
7a8e3164
...
...
@@ -86,24 +86,28 @@
</div>
</div>
<!-- Confirmation Modal -->
<div
class=
"modal fade"
id=
"mdl_confirmation"
tabindex=
"-1"
aria-hidden=
"true"
>
<div
class=
"modal-dialog"
>
<div
class=
"modal-content shadow-lg"
>
<div
class=
"modal-header"
>
<h5
class=
"modal-title"
>
Confirm
</h5>
<button
type=
"button"
class=
"btn-close"
data-bs-dismiss=
"modal"
aria-label=
"Close"
></button>
</div>
<div
class=
"modal-body fw-bold"
id=
"lbl_confirmation_body"
>
Placeholder text
</div>
<div
class=
"modal-footer justify-content-center"
>
<button
type=
"button"
class=
"btn btn-primary me-3"
id=
"btn_confirm_left"
>
Cancel
</button>
<button
type=
"button"
class=
"btn btn-secondary"
id=
"btn_confirm_right"
>
Confirm
</button>
<!-- Confirmation Modal -->
<div
class=
"modal fade"
id=
"main_confirmation_modal"
tabindex=
"-1"
aria-hidden=
"true"
>
<div
class=
"modal-dialog"
>
<div
class=
"modal-content shadow-lg"
>
<div
class=
"modal-header"
>
<h5
class=
"modal-title"
>
Confirm
</h5>
<button
type=
"button"
class=
"btn-close"
data-bs-dismiss=
"modal"
aria-label=
"Close"
></button>
</div>
<div
class=
"modal-body fw-bold"
id=
"main_modal_body"
>
Placeholder text
</div>
<div
class=
"modal-footer justify-content-center"
>
<button
type=
"button"
class=
"btn btn-danger me-3"
id=
"btn_confirm_left"
>
Delete
</button>
<button
type=
"button"
class=
"btn btn-secondary"
id=
"btn_confirm_right"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div
class=
"modal fade"
id=
"mdl_notification"
tabindex=
"-1"
aria-labelledby=
"lbl_notification_title"
...
...
emT3Web/View.TimeEntries.dfm
View file @
7a8e3164
object FTimeEntries: TFTimeEntries
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel
Left = 90
Top = 32
Width =
69
Width =
3
Height = 15
ElementID = 'lbl_validation_message'
ElementFont = efCSS
...
...
@@ -26,7 +27,7 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtWeekOf'
WidthPercent = 100.000000000000000000
On
Change = edtWeekOfChange
On
Exit = edtWeekOfExit
end
object edtStartDate: TWebEdit
Left = 245
...
...
@@ -41,7 +42,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtStartDate'
WidthPercent = 100.000000000000000000
OnChange = edtStartDateChange
end
object edtEndDate: TWebEdit
Left = 415
...
...
@@ -56,7 +56,6 @@ object FTimeEntries: TFTimeEntries
ShowFocus = False
Text = 'edtEndDate'
WidthPercent = 100.000000000000000000
OnChange = edtEndDateChange
end
object btnAddEntry: TWebButton
Left = 440
...
...
@@ -72,6 +71,33 @@ object FTimeEntries: TFTimeEntries
WidthPercent = 100.000000000000000000
OnClick = btnAddEntryClick
end
object btnDeleteEntry: TWebButton
Left = 338
Top = 24
Width = 96
Height = 25
Caption = 'Delete Entry'
ChildOrder = 5
ElementID = 'btn_delete_entry'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnDeleteEntryClick
end
object btnSearchRange: TWebButton
Left = 236
Top = 24
Width = 96
Height = 25
Caption = 'Search Range'
ChildOrder = 6
ElementID = 'btn_search_range'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnSearchRangeClick
end
object xdwcTimeEntries: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 460
...
...
@@ -107,5 +133,11 @@ object FTimeEntries: TFTimeEntries
object xdwdsTimeEntriessummary: TStringField
FieldName = 'summary'
end
object xdwdsTimeEntriesplace: TStringField
FieldName = 'place'
end
object xdwdsTimeEntriesplaceDesc: TStringField
FieldName = 'placeDesc'
end
end
end
emT3Web/View.TimeEntries.html
View file @
7a8e3164
...
...
@@ -7,22 +7,74 @@
<div
class=
"d-flex align-items-center gap-2 flex-nowrap"
>
<label
for=
"edt_week_of"
class=
"form-label mb-0 text-nowrap small"
>
Week of
</label>
<input
id=
"edt_week_of"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
>
<input
id=
"edt_week_of"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
/
>
</div>
<div
class=
"d-flex align-items-center gap-2 flex-nowrap"
>
<label
for=
"edt_start_date"
class=
"form-label mb-0 text-nowrap small"
>
Start
</label>
<input
id=
"edt_start_date"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
>
<input
id=
"edt_start_date"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
/
>
</div>
<div
class=
"d-flex align-items-center gap-2 flex-nowrap"
>
<label
for=
"edt_end_date"
class=
"form-label mb-0 text-nowrap small"
>
End
</label>
<input
id=
"edt_end_date"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
>
<input
id=
"edt_end_date"
type=
"date"
class=
"form-control form-control-sm time-date-picker"
/
>
</div>
<button
id=
"btn_add_entry"
class=
"btn btn-sm btn-success text-nowrap"
>
Add Entry
</button>
<button
id=
"btn_search_range"
class=
"btn btn-sm btn-primary text-nowrap"
>
Search Range
</button>
<button
id=
"btn_add_entry"
class=
"btn btn-sm btn-success text-nowrap"
>
Add Entry
</button>
<button
id=
"btn_delete_entry"
class=
"btn btn-sm btn-danger text-nowrap"
disabled
>
Delete Entry
</button>
</div>
</div>
<div
id=
"lbl_validation_message"
class=
"alert alert-danger py-1 px-2 mb-2 d-none small"
></div>
<div
id=
"time_entries_table_host"
class=
"flex-grow-1 min-h-0 overflow-auto"
></div>
<div
class=
"offcanvas offcanvas-end"
tabindex=
"-1"
id=
"task_picker_offcanvas"
aria-labelledby=
"task_picker_title"
>
<div
class=
"offcanvas-header"
>
<h5
class=
"offcanvas-title"
id=
"task_picker_title"
>
Find Task
</h5>
<button
type=
"button"
class=
"btn-close"
data-bs-dismiss=
"offcanvas"
aria-label=
"Close"
></button>
</div>
<div
class=
"offcanvas-body"
>
<div
class=
"mb-3"
>
<label
for=
"task_picker_customer"
class=
"form-label"
>
Customer
</label>
<select
id=
"task_picker_customer"
class=
"form-select form-select-sm"
>
<option
value=
""
></option>
</select>
</div>
<div
class=
"mb-3"
>
<label
for=
"task_picker_project"
class=
"form-label"
>
Project
</label>
<select
id=
"task_picker_project"
class=
"form-select form-select-sm"
disabled
>
<option
value=
""
></option>
</select>
</div>
<div
class=
"mb-3"
>
<label
for=
"task_picker_task"
class=
"form-label"
>
Task
</label>
<select
id=
"task_picker_task"
class=
"form-select form-select-sm"
disabled
>
<option
value=
""
></option>
</select>
</div>
<div
id=
"task_picker_status"
class=
"alert alert-danger py-1 px-2 mb-3 d-none small"
></div>
<div
class=
"d-flex justify-content-end gap-2"
>
<button
type=
"button"
class=
"btn btn-outline-secondary btn-sm"
data-bs-dismiss=
"offcanvas"
>
Cancel
</button>
<button
type=
"button"
class=
"btn btn-primary btn-sm"
id=
"btn_task_picker_save"
disabled
>
Save
</button>
</div>
</div>
</div>
</div>
emT3Web/View.TimeEntries.pas
View file @
7a8e3164
...
...
@@ -4,8 +4,8 @@ interface
uses
System
.
SysUtils
,
System
.
Classes
,
System
.
DateUtils
,
JS
,
Web
,
WEBLib
.
Graphics
,
WEBLib
.
Controls
,
WEBLib
.
Forms
,
WEBLib
.
Dialogs
,
Vcl
.
Controls
,
Vcl
.
StdCtrls
,
WEBLib
.
StdCtrls
,
WEBLib
.
ExtCtrls
,
XData
.
Web
.
Client
,
ConnectionModule
,
XData
.
Web
.
JsonDataset
,
Data
.
DB
,
XData
.
Web
.
Dataset
;
WEBLib
.
Forms
,
WEBLib
.
Dialogs
,
Vcl
.
Controls
,
Vcl
.
StdCtrls
,
WEBLib
.
StdCtrls
,
WEBLib
.
ExtCtrls
,
System
.
StrUtils
,
XData
.
Web
.
Client
,
ConnectionModule
,
XData
.
Web
.
JsonDataset
,
Data
.
DB
,
XData
.
Web
.
Dataset
,
uTaskPickerOffCanvas
;
type
TFTimeEntries
=
class
(
TWebForm
)
...
...
@@ -25,11 +25,15 @@ type
xdwdsTimeEntriescategoryDesc
:
TStringField
;
btnAddEntry
:
TWebButton
;
lblValidationMessage
:
TWebLabel
;
procedure
edtWeekOfChange
(
Sender
:
TObject
);
procedure
edtStartDateChange
(
Sender
:
TObject
);
procedure
edtEndDateChange
(
Sender
:
TObject
);
xdwdsTimeEntriesplace
:
TStringField
;
xdwdsTimeEntriesplaceDesc
:
TStringField
;
btnDeleteEntry
:
TWebButton
;
btnSearchRange
:
TWebButton
;
procedure
WebFormCreate
(
Sender
:
TObject
);
[
async
]
procedure
btnAddEntryClick
(
Sender
:
TObject
);
procedure
btnDeleteEntryClick
(
Sender
:
TObject
);
procedure
edtWeekOfExit
(
Sender
:
TObject
);
[
async
]
procedure
btnSearchRangeClick
(
Sender
:
TObject
);
private
FUserId
:
string
;
FUserName
:
string
;
...
...
@@ -37,6 +41,7 @@ type
FEndDate
:
string
;
FTaskOptions
:
TJSArray
;
FCategoryOptions
:
TJSArray
;
FPlaceOptions
:
TJSArray
;
FUpdatingDates
:
Boolean
;
FPendingScrollTop
:
Integer
;
FPendingScrollLeft
:
Integer
;
...
...
@@ -44,6 +49,7 @@ type
FPendingEntryId
:
string
;
FLastMouseDownRowIndex
:
Integer
;
FLastMouseDownTime
:
Double
;
FTaskPickerOffCanvas
:
TTaskPickerOffCanvas
;
[
async
]
procedure
LoadTimeEntries
;
[
async
]
function
AddTimeEntry
:
Boolean
;
procedure
RenderTable
;
...
...
@@ -69,10 +75,15 @@ type
procedure
ClearRowValidation
(
AIndex
:
Integer
);
procedure
ShowRowValidationMessage
(
AIndex
:
Integer
;
const
AMessage
:
string
);
procedure
HideRowValidationMessage
;
[
async
]
procedure
SaveRow
(
AIndex
:
Integer
);
procedure
SetTopControlsEnabled
(
AEnabled
:
Boolean
);
function
GetTargetRowIndex
(
ATarget
:
TJSHTMLElement
):
Integer
;
procedure
DocumentMouseDown
(
Event
:
TJSEvent
);
[
async
]
procedure
TaskPickerButtonClick
(
Event
:
TJSEvent
);
procedure
TaskPickerTaskSelected
(
ARowIndex
:
Integer
;
const
ATaskId
,
ATaskDisplay
:
string
);
procedure
ApplyActiveRowState
;
procedure
EditorBlur
(
Event
:
TJSEvent
);
[
async
]
procedure
SaveField
(
AIndex
:
Integer
;
const
AFieldName
:
string
);
[
async
]
procedure
DeleteSelectedEntry
;
public
end
;
...
...
@@ -96,6 +107,7 @@ begin
FTaskOptions
:=
TJSArray
.
new
;
FCategoryOptions
:=
TJSArray
.
new
;
FPlaceOptions
:=
TJSArray
.
new
;
FUpdatingDates
:=
False
;
FPendingScrollTop
:=
0
;
FPendingScrollLeft
:=
0
;
...
...
@@ -104,6 +116,8 @@ begin
FLastMouseDownRowIndex
:=
-
1
;
FLastMouseDownTime
:=
0
;
FTaskPickerOffCanvas
:=
TTaskPickerOffCanvas
.
Create
(
xdwcTimeEntries
,
@
TaskPickerTaskSelected
);
document
.
addEventListener
(
'mousedown'
,
TJSEventHandler
(@
DocumentMouseDown
));
payload
:=
AuthService
.
TokenPayload
;
...
...
@@ -117,6 +131,7 @@ begin
end
;
SetTimeEntriesLabel
(
FUserName
);
btnDeleteEntry
.
Enabled
:=
False
;
anchorDate
:=
Application
.
Parameters
.
Values
[
'date'
];
if
anchorDate
=
''
then
...
...
@@ -196,32 +211,19 @@ begin
LoadTimeEntries
;
end
;
procedure
TFTimeEntries
.
edtWeekOfChange
(
Sender
:
TObject
);
begin
if
FUpdatingDates
then
Exit
;
SetWeekRangeFromAnchor
(
edtWeekOf
.
Text
,
True
);
end
;
procedure
TFTimeEntries
.
edtStartDateChange
(
Sender
:
TObject
);
procedure
TFTimeEntries
.
edtWeekOfExit
(
Sender
:
TObject
);
begin
if
FUpdatingDates
then
Exit
;
FStartDate
:=
edtStartDate
.
Text
;
LoadTimeEntries
;
end
;
procedure
TFTimeEntries
.
edtEndDateChange
(
Sender
:
TObject
);
begin
if
FUpdatingDates
then
if
edtWeekOf
.
Text
=
''
then
Exit
;
FEndDate
:=
edtEndDate
.
Text
;
LoadTimeEntries
;
SetWeekRangeFromAnchor
(
edtWeekOf
.
Text
,
True
);
end
;
procedure
TFTimeEntries
.
GotoRowIndex
(
AIndex
:
Integer
);
var
i
:
Integer
;
...
...
@@ -302,6 +304,7 @@ begin
FTaskOptions
:=
TJSArray
(
resultObj
[
'taskOptions'
]);
FCategoryOptions
:=
TJSArray
(
resultObj
[
'categoryOptions'
]);
FPlaceOptions
:=
TJSArray
(
resultObj
[
'placeOptions'
]);
itemsArray
:=
TJSArray
(
resultObj
[
'items'
]);
xdwdsTimeEntries
.
Close
;
...
...
@@ -311,6 +314,7 @@ begin
FActiveRowIndex
:=
-
1
;
HideRowValidationMessage
;
SetTopControlsEnabled
(
True
);
btnDeleteEntry
.
Enabled
:=
False
;
RenderTable
;
finally
...
...
@@ -322,6 +326,7 @@ end;
[
async
]
function
TFTimeEntries
.
AddTimeEntry
:
Boolean
;
var
response
:
TXDataClientResponse
;
resultObj
:
TJSObject
;
begin
Result
:=
False
;
...
...
@@ -337,15 +342,25 @@ begin
Exit
;
end
;
if
(
FActiveRowIndex
>=
0
)
and
(
not
ValidateRow
(
FActiveRowIndex
))
then
begin
ApplyRowValidation
(
FActiveRowIndex
);
ShowRowValidationMessage
(
FActiveRowIndex
,
'Complete required fields before adding another entry.'
);
SetTopControlsEnabled
(
False
);
Exit
;
end
;
try
response
:=
await
(
xdwcTimeEntries
.
RawInvokeAsync
(
'ITimeEntryService.AddTimeEntry'
,
[
FUserId
,
edtWeekOf
.
Text
]
));
FPendingEntryId
:=
JS
.
toString
(
response
.
Result
);
resultObj
:=
TJSObject
(
response
.
Result
);
FPendingEntryId
:=
string
(
resultObj
[
'value'
]);
console
.
log
(
'AddTimeEntry server entry id='
+
FPendingEntryId
);
console
.
log
(
'AddTimeEntry response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
Result
:=
True
;
except
on
E
:
EXDataClientRequestException
do
...
...
@@ -407,7 +422,7 @@ var
'value="'
+
HtmlEncode
(
Value
)
+
'">'
;
end
;
function
SelectList
(
const
FieldName
,
CurrentValue
,
CurrentDisplay
:
string
;
const
AIdx
:
Integer
;
const
Items
:
TJSArray
;
const
ValueProp
,
DisplayProp
:
string
):
string
;
function
SelectList
(
const
FieldName
,
CurrentValue
,
CurrentDisplay
:
string
;
const
AIdx
:
Integer
;
const
Items
:
TJSArray
;
const
ValueProp
,
DisplayProp
:
string
;
AAllowBlank
,
AShowTaskPicker
:
Boolean
):
string
;
var
i
:
Integer
;
optionObj
:
TJSObject
;
...
...
@@ -417,43 +432,60 @@ var
begin
triggerId
:=
'time_dd_'
+
FieldName
+
'_'
+
IntToStr
(
AIdx
);
Result
:=
'<div class="dropdown w-100">'
+
'<button id="'
+
triggerId
+
'" class="btn btn-sm border w-100 d-flex justify-content-between align-items-center text-start time-dd-toggle btn-light" '
+
'type="button" data-bs-toggle="dropdown" aria-expanded="false" '
+
Result
:=
''
;
if
AShowTaskPicker
then
Result
:=
Result
+
'<div class="input-group input-group-sm flex-nowrap w-100">'
;
Result
:=
Result
+
'<div class="dropdown flex-grow-1" style="min-width:0;">'
+
'<button id="'
+
triggerId
+
'" class="btn btn-sm border w-100 d-flex justify-content-between align-items-center text-start time-dd-toggle btn-light'
+
IfThen
(
AShowTaskPicker
,
' rounded-end-0'
,
''
)
+
'" '
+
'type="button" data-bs-toggle="dropdown" aria-expanded="false" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" data-field="'
+
FieldName
+
'">'
+
'<span class="time-dd-label text-truncate">'
+
HtmlEncode
(
CurrentDisplay
)
+
'</span>'
+
'<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>'
+
'</button>'
+
'<div class="dropdown-menu w-100 p-0 overflow-hidden">'
;
Result
:=
Result
+
'<button type="button" class="dropdown-item time-dd-item" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" '
+
'data-field="'
+
FieldName
+
'" '
+
'data-value="" '
+
'data-display="" '
+
'data-trigger-id="'
+
triggerId
+
'"></button>'
;
'<div class="dropdown-menu w-100 p-0 pe-1 overflow-hidden">'
;
if
AAllowBlank
then
begin
Result
:=
Result
+
'<button type="button" class="dropdown-item time-dd-item" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" '
+
'data-field="'
+
FieldName
+
'" '
+
'data-value="" '
+
'data-display="" '
+
'data-trigger-id="'
+
triggerId
+
'"> </button>'
;
end
;
for
i
:=
0
to
Items
.
length
-
1
do
begin
optionObj
:=
TJSObject
(
Items
[
i
]);
optionValue
:=
string
(
optionObj
[
ValueProp
]);
optionDisplay
:=
string
(
optionObj
[
DisplayProp
]);
Result
:=
Result
+
'<button type="button" class="dropdown-item time-dd-item" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" '
+
'data-field="'
+
FieldName
+
'" '
+
'data-value="'
+
HtmlEncode
(
optionValue
)
+
'" '
+
'data-display="'
+
HtmlEncode
(
optionDisplay
)
+
'" '
+
'data-trigger-id="'
+
triggerId
+
'">'
+
HtmlEncode
(
optionDisplay
)
+
'</button>'
;
end
;
for
i
:=
0
to
Items
.
length
-
1
do
begin
optionObj
:=
TJSObject
(
Items
[
i
]);
optionValue
:=
string
(
optionObj
[
ValueProp
]);
optionDisplay
:=
string
(
optionObj
[
DisplayProp
]);
Result
:=
Result
+
'<button type="button" class="dropdown-item time-dd-item" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" '
+
'data-field="'
+
FieldName
+
'" '
+
'data-value="'
+
HtmlEncode
(
optionValue
)
+
'" '
+
'data-display="'
+
HtmlEncode
(
optionDisplay
)
+
'" '
+
'data-trigger-id="'
+
triggerId
+
'">'
+
HtmlEncode
(
optionDisplay
)
+
'</button>'
;
end
;
Result
:=
Result
+
'</div>'
+
'</div>'
;
if
AShowTaskPicker
then
begin
Result
:=
Result
+
'<button type="button" class="btn btn-outline-secondary time-task-picker-btn flex-shrink-0 text-nowrap" '
+
'data-idx="'
+
IntToStr
(
AIdx
)
+
'" title="Find task">Find</button>'
+
'</div>'
;
end
;
end
;
function
SummaryTextArea
(
const
FieldName
,
Value
:
string
;
const
AIdx
:
Integer
):
string
;
...
...
@@ -470,12 +502,13 @@ begin
html
:=
'<div class="time-vscroll">'
+
'<div class="time-hscroll">'
+
'<table class="table table-sm table-bordered align-middle mb-0 time-table" style="min-width: 1
55
0px; table-layout: fixed;">'
+
'<table class="table table-sm table-bordered align-middle mb-0 time-table" style="min-width: 1
71
0px; table-layout: fixed;">'
+
'<colgroup>'
+
'<col style="width:140px">'
+
// Date
'<col style="width:500px">'
+
// Task
'<col style="width:90px">'
+
// Hours
'<col style="width:150px">'
+
// Time
'<col style="width:160px">'
+
// Place
'<col style="width:190px">'
+
// Category
'<col style="width:480px">'
+
// Summary
'</colgroup>'
+
...
...
@@ -484,6 +517,7 @@ begin
Th
(
'Task'
)
+
Th
(
'Hours'
)
+
Th
(
'Time'
)
+
Th
(
'Place'
)
+
Th
(
'Category'
)
+
Th
(
'Summary'
)
+
'</tr></thead><tbody>'
;
...
...
@@ -497,21 +531,23 @@ begin
else
hoursText
:=
FormatFloat
(
'0.##'
,
xdwdsTimeEntrieshours
.
AsFloat
);
html
:=
html
+
'<tr class="time-row-selectable" data-idx="'
+
IntToStr
(
rowIdx
)
+
'" data-entry-id="'
+
IntToStr
(
xdwdsTimeEntriesentryId
.
AsInteger
)
+
'" data-task-id="'
+
xdwdsTimeEntriestaskId
.
AsString
+
'">'
+
TdNowrap
(
DateInput
(
'taskDate'
,
xdwdsTimeEntriestaskDate
.
AsString
,
rowIdx
))
+
TdNowrap
(
SelectList
(
'taskId'
,
xdwdsTimeEntriestaskId
.
AsString
,
xdwdsTimeEntriestaskDisplay
.
AsString
,
rowIdx
,
FTaskOptions
,
'taskId'
,
'taskDisplay'
))
+
TdNowrap
(
HoursInput
(
'hours'
,
hoursText
,
rowIdx
))
+
TdNowrap
(
TextInput
(
'taskTime'
,
xdwdsTimeEntriestaskTime
.
AsString
,
rowIdx
))
+
TdNowrap
(
SelectList
(
'category'
,
xdwdsTimeEntriescategory
.
AsString
,
xdwdsTimeEntriescategoryDesc
.
AsString
,
rowIdx
,
FCategoryOptions
,
'code'
,
'codeDesc'
))
+
TdWrap
(
SummaryTextArea
(
'summary'
,
xdwdsTimeEntriessummary
.
AsString
,
rowIdx
))
+
'</tr>'
;
html
:=
html
+
'<tr class="time-row-selectable" data-idx="'
+
IntToStr
(
rowIdx
)
+
'" data-entry-id="'
+
IntToStr
(
xdwdsTimeEntriesentryId
.
AsInteger
)
+
'" data-task-id="'
+
xdwdsTimeEntriestaskId
.
AsString
+
'">'
+
TdNowrap
(
DateInput
(
'taskDate'
,
xdwdsTimeEntriestaskDate
.
AsString
,
rowIdx
))
+
TdNowrap
(
SelectList
(
'taskId'
,
xdwdsTimeEntriestaskId
.
AsString
,
xdwdsTimeEntriestaskDisplay
.
AsString
,
rowIdx
,
FTaskOptions
,
'taskId'
,
'taskDisplay'
,
False
,
True
))
+
TdNowrap
(
HoursInput
(
'hours'
,
hoursText
,
rowIdx
))
+
TdNowrap
(
TextInput
(
'taskTime'
,
xdwdsTimeEntriestaskTime
.
AsString
,
rowIdx
))
+
TdNowrap
(
SelectList
(
'place'
,
xdwdsTimeEntriesplace
.
AsString
,
xdwdsTimeEntriesplaceDesc
.
AsString
,
rowIdx
,
FPlaceOptions
,
'code'
,
'codeDesc'
,
True
,
False
))
+
TdNowrap
(
SelectList
(
'category'
,
xdwdsTimeEntriescategory
.
AsString
,
xdwdsTimeEntriescategoryDesc
.
AsString
,
rowIdx
,
FCategoryOptions
,
'code'
,
'codeDesc'
,
False
,
False
))
+
TdWrap
(
SummaryTextArea
(
'summary'
,
xdwdsTimeEntriessummary
.
AsString
,
rowIdx
))
+
'</tr>'
;
xdwdsTimeEntries
.
Next
;
Inc
(
rowIdx
);
end
;
html
:=
html
+
'</tbody></table></div></div>'
;
SetTotalRowsLabel
(
rowIdx
);
host
.
innerHTML
:=
html
;
BindTableEditors
;
...
...
@@ -519,6 +555,7 @@ begin
EnableColumnResize
;
RestoreTableScroll
;
ApplyPendingEntryFocus
;
ApplyActiveRowState
;
end
;
procedure
TFTimeEntries
.
BindTableEditors
;
...
...
@@ -535,6 +572,7 @@ begin
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
addEventListener
(
'input'
,
TJSEventHandler
(@
EditorInput
));
el
.
addEventListener
(
'blur'
,
TJSEventHandler
(@
EditorBlur
));
end
;
nodes
:=
document
.
querySelectorAll
(
'.time-dd-item'
);
...
...
@@ -545,6 +583,14 @@ begin
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
DropdownItemClick
));
end
;
nodes
:=
document
.
querySelectorAll
(
'.time-task-picker-btn'
);
console
.
log
(
'BindTableEditors: time-task-picker-btn count='
+
IntToStr
(
nodes
.
length
));
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
TaskPickerButtonClick
));
end
;
nodes
:=
document
.
querySelectorAll
(
'.time-row-selectable'
);
console
.
log
(
'BindTableEditors: time-row-selectable count='
+
IntToStr
(
nodes
.
length
));
for
i
:=
0
to
nodes
.
length
-
1
do
...
...
@@ -577,6 +623,7 @@ begin
Exit
;
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
GotoRowIndex
(
idx
);
if
xdwdsTimeEntries
.
Eof
then
...
...
@@ -616,65 +663,6 @@ begin
end
;
[
async
]
procedure
TFTimeEntries
.
SaveRow
(
AIndex
:
Integer
);
var
saveObj
:
TJSObject
;
response
:
TXDataClientResponse
;
rowEl
:
TJSHTMLElement
;
nodes
:
TJSNodeList
;
i
:
Integer
;
el
:
TJSHTMLElement
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
AIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
if
not
ValidateRow
(
AIndex
)
then
begin
ApplyRowValidation
(
AIndex
);
ShowRowValidationMessage
(
AIndex
,
'Complete required fields before leaving this row.'
);
Exit
;
end
;
saveObj
:=
TJSObject
.
new
;
saveObj
[
'entryId'
]
:=
xdwdsTimeEntriesentryId
.
AsString
;
saveObj
[
'userId'
]
:=
FUserId
;
saveObj
[
'taskDate'
]
:=
xdwdsTimeEntriestaskDate
.
AsString
;
saveObj
[
'taskId'
]
:=
xdwdsTimeEntriestaskId
.
AsString
;
saveObj
[
'hours'
]
:=
xdwdsTimeEntrieshours
.
AsFloat
;
saveObj
[
'taskTime'
]
:=
xdwdsTimeEntriestaskTime
.
AsString
;
saveObj
[
'category'
]
:=
xdwdsTimeEntriescategory
.
AsString
;
saveObj
[
'summary'
]
:=
xdwdsTimeEntriessummary
.
AsString
;
try
response
:=
await
(
xdwcTimeEntries
.
RawInvokeAsync
(
'ITimeEntryService.SaveTimeEntry'
,
[
saveObj
]
));
console
.
log
(
'SaveRow response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
rowEl
:=
TJSHTMLElement
(
document
.
querySelector
(
'tr[data-idx="'
+
IntToStr
(
AIndex
)
+
'"]'
));
nodes
:=
rowEl
.
querySelectorAll
(
'[data-unsaved-data="1"]'
);
for
i
:=
0
to
nodes
.
length
-
1
do
begin
el
:=
TJSHTMLElement
(
nodes
.
item
(
i
));
el
.
removeAttribute
(
'data-unsaved-data'
);
end
;
ClearRowValidation
(
AIndex
);
except
on
E
:
EXDataClientRequestException
do
begin
console
.
log
(
'SaveRow ERROR: '
+
E
.
ErrorResult
.
ErrorMessage
);
Utils
.
ShowErrorModal
(
E
.
ErrorResult
.
ErrorMessage
);
end
;
end
;
end
;
procedure
TFTimeEntries
.
RowClick
(
Event
:
TJSEvent
);
var
rowEl
:
TJSHTMLElement
;
...
...
@@ -686,7 +674,22 @@ begin
if
idx
<
0
then
Exit
;
if
(
FActiveRowIndex
>=
0
)
and
(
idx
<>
FActiveRowIndex
)
and
(
not
ValidateRow
(
FActiveRowIndex
))
then
begin
Event
.
preventDefault
;
Event
.
stopPropagation
;
ApplyRowValidation
(
FActiveRowIndex
);
ShowRowValidationMessage
(
FActiveRowIndex
,
'Complete required fields before leaving this row.'
);
ApplyActiveRowState
;
Exit
;
end
;
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
ApplyActiveRowState
;
end
;
...
...
@@ -713,6 +716,7 @@ begin
if
not
ValidateRow
(
idx
)
then
begin
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
ApplyRowValidation
(
idx
);
ShowRowValidationMessage
(
idx
,
'Complete required fields before leaving this row.'
);
SetTopControlsEnabled
(
False
);
...
...
@@ -721,9 +725,6 @@ begin
ClearRowValidation
(
idx
);
SetTopControlsEnabled
(
True
);
if
Assigned
(
rowEl
.
querySelector
(
'[data-unsaved-data="1"]'
))
then
SaveRow
(
idx
);
end
,
0
);
...
...
@@ -770,6 +771,11 @@ begin
xdwdsTimeEntriestaskId
.
AsString
:=
newValue
;
xdwdsTimeEntriestaskDisplay
.
AsString
:=
newDisplay
;
end
else
if
SameText
(
fieldName
,
'place'
)
then
begin
xdwdsTimeEntriesplace
.
AsString
:=
newValue
;
xdwdsTimeEntriesplaceDesc
.
AsString
:=
newDisplay
;
end
else
if
SameText
(
fieldName
,
'category'
)
then
begin
xdwdsTimeEntriescategory
.
AsString
:=
newValue
;
...
...
@@ -781,13 +787,19 @@ begin
xdwdsTimeEntries
.
Post
;
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
btn
:=
TJSHTMLElement
(
document
.
getElementById
(
triggerId
));
btn
.
setAttribute
(
'data-unsaved-data'
,
'1'
);
if
Assigned
(
btn
)
then
begin
labelEl
:=
TJSHTMLElement
(
btn
.
querySelector
(
'.time-dd-label'
));
if
Assigned
(
labelEl
)
then
labelEl
.
textContent
:=
newDisplay
;
labelEl
:=
TJSHTMLElement
(
btn
.
querySelector
(
'.time-dd-label'
));
labelEl
.
textContent
:=
newDisplay
;
btn
.
focus
;
btn
.
focus
;
end
;
SaveField
(
idx
,
fieldName
);
if
ValidateRow
(
idx
)
then
begin
...
...
@@ -853,11 +865,21 @@ begin
end
;
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
SetTopControlsEnabled
(
False
);
ApplyActiveRowState
;
firstEditor
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'[data-field="taskId"]'
));
if
not
Assigned
(
firstEditor
)
then
firstEditor
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'[data-field="hours"]'
));
firstEditor
:=
TJSHTMLElement
(
rowEl
.
querySelector
(
'[data-field="taskDate"]'
));
if
Assigned
(
firstEditor
)
then
firstEditor
.
focus
;
begin
asm
firstEditor
.
scrollIntoView
(
{ behavior: 'auto', block: 'nearest', inline: 'nearest' }
);
firstEditor
.
focus
();
end
;
end
;
FPendingEntryId
:=
''
;
end
;
...
...
@@ -975,10 +997,11 @@ begin
el
:=
ATarget
;
while
Assigned
(
el
)
do
begin
idxText
:=
string
(
el
.
getAttribute
(
'data-idx'
));
if
idxText
<>
''
then
if
el
.
hasAttribute
(
'data-idx'
)
then
begin
idxText
:=
string
(
el
.
getAttribute
(
'data-idx'
));
Result
:=
StrToIntDef
(
idxText
,
-
1
);
if
Result
>=
0
then
Exit
;
end
;
...
...
@@ -989,8 +1012,32 @@ end;
procedure
TFTimeEntries
.
DocumentMouseDown
(
Event
:
TJSEvent
);
var
targetEl
:
TJSHTMLElement
;
targetRowIndex
:
Integer
;
begin
FLastMouseDownRowIndex
:=
GetTargetRowIndex
(
TJSHTMLElement
(
Event
.
target
));
targetEl
:=
TJSHTMLElement
(
Event
.
target
);
targetRowIndex
:=
GetTargetRowIndex
(
targetEl
);
if
(
FActiveRowIndex
>=
0
)
and
(
targetRowIndex
>=
0
)
and
(
targetRowIndex
<>
FActiveRowIndex
)
and
(
not
ValidateRow
(
FActiveRowIndex
))
then
begin
Event
.
preventDefault
;
Event
.
stopPropagation
;
ApplyRowValidation
(
FActiveRowIndex
);
ShowRowValidationMessage
(
FActiveRowIndex
,
'Complete required fields before leaving this row.'
);
SetTopControlsEnabled
(
False
);
FLastMouseDownRowIndex
:=
FActiveRowIndex
;
FLastMouseDownTime
:=
TJSDate
.
now
;
ApplyActiveRowState
;
Exit
;
end
;
FLastMouseDownRowIndex
:=
targetRowIndex
;
FLastMouseDownTime
:=
TJSDate
.
now
;
end
;
...
...
@@ -1055,7 +1102,7 @@ begin
if
await
(
AddTimeEntry
)
then
begin
CaptureTableScroll
;
LoadTimeEntries
;
await
(
LoadTimeEntries
)
;
end
;
finally
Utils
.
HideSpinner
(
'spinner'
);
...
...
@@ -1063,6 +1110,52 @@ begin
end
;
procedure
TFTimeEntries
.
btnDeleteEntryClick
(
Sender
:
TObject
);
begin
if
FActiveRowIndex
<
0
then
begin
Utils
.
ShowErrorModal
(
'Select a time entry to delete.'
);
Exit
;
end
;
Utils
.
ShowConfirmationModal
(
'Delete the selected time entry?'
,
'Delete'
,
'Cancel'
,
procedure
(
AConfirmed
:
Boolean
)
begin
if
AConfirmed
then
DeleteSelectedEntry
;
end
);
end
;
[
async
]
procedure
TFTimeEntries
.
btnSearchRangeClick
(
Sender
:
TObject
);
begin
if
edtStartDate
.
Text
=
''
then
begin
Utils
.
ShowErrorModal
(
'Select a start date.'
);
Exit
;
end
;
if
edtEndDate
.
Text
=
''
then
begin
Utils
.
ShowErrorModal
(
'Select an end date.'
);
Exit
;
end
;
if
IsoToDate
(
edtEndDate
.
Text
)
<
IsoToDate
(
edtStartDate
.
Text
)
then
begin
Utils
.
ShowErrorModal
(
'End date cannot be before start date.'
);
Exit
;
end
;
FStartDate
:=
edtStartDate
.
Text
;
FEndDate
:=
edtEndDate
.
Text
;
await
(
LoadTimeEntries
);
end
;
procedure
TFTimeEntries
.
CaptureTableScroll
;
begin
asm
...
...
@@ -1091,7 +1184,236 @@ begin
edtWeekOf
.
Enabled
:=
AEnabled
;
edtStartDate
.
Enabled
:=
AEnabled
;
edtEndDate
.
Enabled
:=
AEnabled
;
btnSearchRange
.
Enabled
:=
AEnabled
;
btnAddEntry
.
Enabled
:=
AEnabled
;
end
;
[
async
]
procedure
TFTimeEntries
.
TaskPickerButtonClick
(
Event
:
TJSEvent
);
var
el
:
TJSHTMLElement
;
idx
:
Integer
;
begin
Event
.
preventDefault
;
Event
.
stopPropagation
;
el
:=
TJSHTMLElement
(
Event
.
currentTarget
);
idx
:=
StrToIntDef
(
string
(
el
.
getAttribute
(
'data-idx'
)),
-
1
);
if
idx
<
0
then
Exit
;
FActiveRowIndex
:=
idx
;
btnDeleteEntry
.
Enabled
:=
True
;
if
not
Assigned
(
FTaskPickerOffCanvas
)
then
FTaskPickerOffCanvas
:=
TTaskPickerOffCanvas
.
Create
(
xdwcTimeEntries
,
@
TaskPickerTaskSelected
);
await
(
FTaskPickerOffCanvas
.
Open
(
FUserId
,
idx
));
end
;
procedure
TFTimeEntries
.
TaskPickerTaskSelected
(
ARowIndex
:
Integer
;
const
ATaskId
,
ATaskDisplay
:
string
);
var
btn
:
TJSHTMLElement
;
triggerId
:
string
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
ARowIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
xdwdsTimeEntries
.
Edit
;
xdwdsTimeEntriestaskId
.
AsString
:=
ATaskId
;
xdwdsTimeEntriestaskDisplay
.
AsString
:=
ATaskDisplay
;
xdwdsTimeEntries
.
Post
;
FActiveRowIndex
:=
ARowIndex
;
btnDeleteEntry
.
Enabled
:=
True
;
CaptureTableScroll
;
RenderTable
;
triggerId
:=
'time_dd_taskId_'
+
IntToStr
(
ARowIndex
);
btn
:=
TJSHTMLElement
(
document
.
getElementById
(
triggerId
));
if
Assigned
(
btn
)
then
btn
.
focus
;
SaveField
(
ARowIndex
,
'taskId'
);
if
ValidateRow
(
ARowIndex
)
then
begin
ClearRowValidation
(
ARowIndex
);
SetTopControlsEnabled
(
True
);
end
else
SetTopControlsEnabled
(
False
);
end
;
procedure
TFTimeEntries
.
ApplyActiveRowState
;
begin
asm
const
activeRowIndex
=
this
.
FActiveRowIndex
;
document
.
querySelectorAll
(
'.time-row-selectable'
).
forEach
(
function
(
row
)
{
const rowIndex = parseInt(row.getAttribute('data-idx') || '-1', 10);
if ((activeRowIndex >= 0) && (rowIndex === activeRowIndex))
row.classList.add('table-active');
else
row.classList.remove('table-active');
}
);
end
;
end
;
procedure
TFTimeEntries
.
EditorBlur
(
Event
:
TJSEvent
);
var
el
:
TJSHTMLElement
;
idx
:
Integer
;
idxStr
:
string
;
fieldName
:
string
;
begin
el
:=
TJSHTMLElement
(
Event
.
target
);
idxStr
:=
string
(
el
.
getAttribute
(
'data-idx'
));
fieldName
:=
string
(
el
.
getAttribute
(
'data-field'
));
if
string
(
el
.
getAttribute
(
'data-unsaved-data'
))
<>
'1'
then
begin
console
.
log
(
'EditorBlur: skip (not unsaved) idx='
+
idxStr
+
' field='
+
fieldName
);
Exit
;
end
;
el
.
removeAttribute
(
'data-unsaved-data'
);
idx
:=
StrToIntDef
(
idxStr
,
-
1
);
if
idx
<
0
then
Exit
;
console
.
log
(
'EditorBlur: SAVE idx='
+
IntToStr
(
idx
)
+
' field='
+
fieldName
);
SaveField
(
idx
,
fieldName
);
end
;
[
async
]
procedure
TFTimeEntries
.
SaveField
(
AIndex
:
Integer
;
const
AFieldName
:
string
);
var
response
:
TXDataClientResponse
;
payload
:
TJSObject
;
fieldValue
:
string
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
GotoRowIndex
(
AIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
if
xdwdsTimeEntriesentryId
.
AsInteger
<=
0
then
Exit
;
fieldValue
:=
''
;
if
SameText
(
AFieldName
,
'taskDate'
)
then
fieldValue
:=
xdwdsTimeEntriestaskDate
.
AsString
else
if
SameText
(
AFieldName
,
'taskId'
)
then
fieldValue
:=
xdwdsTimeEntriestaskId
.
AsString
else
if
SameText
(
AFieldName
,
'hours'
)
then
begin
if
xdwdsTimeEntrieshours
.
IsNull
then
fieldValue
:=
''
else
fieldValue
:=
xdwdsTimeEntrieshours
.
AsString
;
end
else
if
SameText
(
AFieldName
,
'taskTime'
)
then
fieldValue
:=
xdwdsTimeEntriestaskTime
.
AsString
else
if
SameText
(
AFieldName
,
'place'
)
then
fieldValue
:=
xdwdsTimeEntriesplace
.
AsString
else
if
SameText
(
AFieldName
,
'category'
)
then
fieldValue
:=
xdwdsTimeEntriescategory
.
AsString
else
if
SameText
(
AFieldName
,
'summary'
)
then
fieldValue
:=
xdwdsTimeEntriessummary
.
AsString
else
Exit
;
payload
:=
TJSObject
.
new
;
payload
[
'entryId'
]
:=
xdwdsTimeEntriesentryId
.
AsInteger
;
payload
[
'userId'
]
:=
FUserId
;
payload
[
'fieldName'
]
:=
AFieldName
;
payload
[
'value'
]
:=
fieldValue
;
console
.
log
(
'SaveField: idx='
+
IntToStr
(
AIndex
)
+
' field='
+
AFieldName
+
' value='
+
fieldValue
);
try
response
:=
await
(
xdwcTimeEntries
.
RawInvokeAsync
(
'ITimeEntryService.SaveTimeEntryField'
,
[
payload
]
));
console
.
log
(
'SaveField: response='
+
string
(
TJSJSON
.
stringify
(
response
.
Result
)));
except
on
E
:
EXDataClientRequestException
do
begin
console
.
log
(
'SaveField ERROR: '
+
E
.
ErrorResult
.
ErrorMessage
);
Utils
.
ShowErrorModal
(
E
.
ErrorResult
.
ErrorMessage
);
end
;
end
;
end
;
[
async
]
procedure
TFTimeEntries
.
DeleteSelectedEntry
;
var
entryId
:
Integer
;
begin
if
not
xdwdsTimeEntries
.
Active
then
Exit
;
if
FActiveRowIndex
<
0
then
begin
Utils
.
ShowErrorModal
(
'Select a time entry to delete.'
);
Exit
;
end
;
GotoRowIndex
(
FActiveRowIndex
);
if
xdwdsTimeEntries
.
Eof
then
Exit
;
entryId
:=
xdwdsTimeEntriesentryId
.
AsInteger
;
if
entryId
<=
0
then
begin
Utils
.
ShowErrorModal
(
'Unable to determine the selected time entry.'
);
Exit
;
end
;
Utils
.
ShowSpinner
(
'spinner'
);
try
try
await
(
xdwcTimeEntries
.
RawInvokeAsync
(
'ITimeEntryService.DeleteTimeEntry'
,
[
FUserId
,
entryId
]
));
except
on
E
:
EXDataClientRequestException
do
begin
console
.
log
(
'DeleteSelectedEntry ERROR: '
+
E
.
ErrorResult
.
ErrorMessage
);
Utils
.
ShowErrorModal
(
E
.
ErrorResult
.
ErrorMessage
);
Exit
;
end
;
end
;
FActiveRowIndex
:=
-
1
;
FPendingEntryId
:=
''
;
HideRowValidationMessage
;
SetTopControlsEnabled
(
True
);
btnDeleteEntry
.
Enabled
:=
False
;
CaptureTableScroll
;
await
(
LoadTimeEntries
);
finally
Utils
.
HideSpinner
(
'spinner'
);
end
;
end
;
end
.
emT3Web/emT3web.dpr
View file @
7a8e3164
...
...
@@ -17,7 +17,8 @@ uses
uDropdownHelpers in 'uDropdownHelpers.pas',
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.TimeEntries in 'View.TimeEntries.pas' {FTimeEntries: TWebForm} {*.html};
View.TimeEntries in 'View.TimeEntries.pas' {FTimeEntries: TWebForm} {*.html},
uTaskPickerOffCanvas in 'uTaskPickerOffCanvas.pas';
{$R *.res}
...
...
emT3Web/emT3web.dproj
View file @
7a8e3164
...
...
@@ -156,6 +156,7 @@
<Form>FTimeEntries</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uTaskPickerOffCanvas.pas"/>
<None Include="index.html"/>
<None Include="css\app.css"/>
<None Include="config\config.json"/>
...
...
emT3Web/uTaskPickerOffCanvas.pas
0 → 100644
View file @
7a8e3164
unit
uTaskPickerOffCanvas
;
interface
uses
System
.
SysUtils
,
JS
,
Web
,
XData
.
Web
.
Client
;
type
TTaskPickedProc
=
reference
to
procedure
(
ARowIndex
:
Integer
;
const
ATaskId
,
ATaskDisplay
:
string
);
TTaskPickerOffCanvas
=
class
private
FClient
:
TXDataWebClient
;
FUserId
:
string
;
FRowIndex
:
Integer
;
FOnTaskPicked
:
TTaskPickedProc
;
FCustomers
:
TJSArray
;
FProjects
:
TJSArray
;
FTasks
:
TJSArray
;
procedure
BindEvents
;
procedure
ClearSelect
(
const
AElementId
:
string
;
ADisabled
:
Boolean
);
procedure
FillSelect
(
const
AElementId
:
string
;
AItems
:
TJSArray
);
function
SelectedValue
(
const
AElementId
:
string
):
string
;
function
FindDisplay
(
AItems
:
TJSArray
;
const
AValue
:
string
):
string
;
procedure
SetStatus
(
const
AMessage
:
string
);
procedure
ClearStatus
;
procedure
SetSaveEnabled
(
AEnabled
:
Boolean
);
procedure
Show
;
procedure
Hide
;
[
async
]
procedure
LoadCustomers
;
[
async
]
procedure
LoadProjects
(
const
ACustomerId
:
string
);
[
async
]
procedure
LoadTasks
(
const
AProjectId
:
string
);
[
async
]
procedure
CustomerChanged
(
Event
:
TJSEvent
);
[
async
]
procedure
ProjectChanged
(
Event
:
TJSEvent
);
procedure
TaskChanged
(
Event
:
TJSEvent
);
procedure
SaveClicked
(
Event
:
TJSEvent
);
public
constructor
Create
(
AClient
:
TXDataWebClient
;
AOnTaskPicked
:
TTaskPickedProc
);
[
async
]
procedure
Open
(
const
AUserId
:
string
;
ARowIndex
:
Integer
);
end
;
implementation
constructor
TTaskPickerOffCanvas
.
Create
(
AClient
:
TXDataWebClient
;
AOnTaskPicked
:
TTaskPickedProc
);
begin
inherited
Create
;
FClient
:=
AClient
;
FOnTaskPicked
:=
AOnTaskPicked
;
FCustomers
:=
TJSArray
.
new
;
FProjects
:=
TJSArray
.
new
;
FTasks
:=
TJSArray
.
new
;
FRowIndex
:=
-
1
;
BindEvents
;
end
;
procedure
TTaskPickerOffCanvas
.
BindEvents
;
var
el
:
TJSHTMLElement
;
begin
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'task_picker_customer'
));
if
Assigned
(
el
)
then
el
.
addEventListener
(
'change'
,
TJSEventHandler
(@
CustomerChanged
));
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'task_picker_project'
));
if
Assigned
(
el
)
then
el
.
addEventListener
(
'change'
,
TJSEventHandler
(@
ProjectChanged
));
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'task_picker_task'
));
if
Assigned
(
el
)
then
el
.
addEventListener
(
'change'
,
TJSEventHandler
(@
TaskChanged
));
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'btn_task_picker_save'
));
if
Assigned
(
el
)
then
el
.
addEventListener
(
'click'
,
TJSEventHandler
(@
SaveClicked
));
end
;
procedure
TTaskPickerOffCanvas
.
ClearSelect
(
const
AElementId
:
string
;
ADisabled
:
Boolean
);
var
selectEl
:
TJSHTMLSelectElement
;
begin
selectEl
:=
TJSHTMLSelectElement
(
document
.
getElementById
(
AElementId
));
if
not
Assigned
(
selectEl
)
then
Exit
;
selectEl
.
innerHTML
:=
'<option value=""></option>'
;
selectEl
.
disabled
:=
ADisabled
;
end
;
procedure
TTaskPickerOffCanvas
.
FillSelect
(
const
AElementId
:
string
;
AItems
:
TJSArray
);
var
selectEl
:
TJSHTMLSelectElement
;
optionEl
:
TJSHTMLOptionElement
;
itemObj
:
TJSObject
;
i
:
Integer
;
begin
selectEl
:=
TJSHTMLSelectElement
(
document
.
getElementById
(
AElementId
));
if
not
Assigned
(
selectEl
)
then
Exit
;
selectEl
.
innerHTML
:=
'<option value=""></option>'
;
for
i
:=
0
to
AItems
.
length
-
1
do
begin
itemObj
:=
TJSObject
(
AItems
[
i
]);
optionEl
:=
TJSHTMLOptionElement
(
document
.
createElement
(
'option'
));
optionEl
.
value
:=
string
(
itemObj
[
'value'
]);
optionEl
.
text
:=
string
(
itemObj
[
'display'
]);
selectEl
.
appendChild
(
optionEl
);
end
;
selectEl
.
disabled
:=
False
;
end
;
function
TTaskPickerOffCanvas
.
SelectedValue
(
const
AElementId
:
string
):
string
;
var
selectEl
:
TJSHTMLSelectElement
;
begin
Result
:=
''
;
selectEl
:=
TJSHTMLSelectElement
(
document
.
getElementById
(
AElementId
));
if
Assigned
(
selectEl
)
then
Result
:=
string
(
selectEl
.
value
);
end
;
function
TTaskPickerOffCanvas
.
FindDisplay
(
AItems
:
TJSArray
;
const
AValue
:
string
):
string
;
var
itemObj
:
TJSObject
;
i
:
Integer
;
begin
Result
:=
''
;
for
i
:=
0
to
AItems
.
length
-
1
do
begin
itemObj
:=
TJSObject
(
AItems
[
i
]);
if
string
(
itemObj
[
'value'
])
=
AValue
then
Exit
(
string
(
itemObj
[
'display'
]));
end
;
end
;
procedure
TTaskPickerOffCanvas
.
SetStatus
(
const
AMessage
:
string
);
var
el
:
TJSHTMLElement
;
begin
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'task_picker_status'
));
if
not
Assigned
(
el
)
then
Exit
;
el
.
innerText
:=
AMessage
;
el
.
classList
.
remove
(
'd-none'
);
end
;
procedure
TTaskPickerOffCanvas
.
ClearStatus
;
var
el
:
TJSHTMLElement
;
begin
el
:=
TJSHTMLElement
(
document
.
getElementById
(
'task_picker_status'
));
if
not
Assigned
(
el
)
then
Exit
;
el
.
innerText
:=
''
;
el
.
classList
.
add
(
'd-none'
);
end
;
procedure
TTaskPickerOffCanvas
.
SetSaveEnabled
(
AEnabled
:
Boolean
);
var
btn
:
TJSHTMLButtonElement
;
begin
btn
:=
TJSHTMLButtonElement
(
document
.
getElementById
(
'btn_task_picker_save'
));
if
Assigned
(
btn
)
then
btn
.
disabled
:=
not
AEnabled
;
end
;
procedure
TTaskPickerOffCanvas
.
Show
;
begin
asm
const
el
=
document
.
getElementById
(
'task_picker_offcanvas'
);
if
(!
el
||
!
window
.
bootstrap
)
return
;
const
oc
=
bootstrap
.
Offcanvas
.
getOrCreateInstance
(
el
);
oc
.
show
();
end
;
end
;
procedure
TTaskPickerOffCanvas
.
Hide
;
begin
asm
const
el
=
document
.
getElementById
(
'task_picker_offcanvas'
);
if
(!
el
||
!
window
.
bootstrap
)
return
;
const
oc
=
bootstrap
.
Offcanvas
.
getOrCreateInstance
(
el
);
oc
.
hide
();
end
;
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
LoadCustomers
;
var
response
:
TXDataClientResponse
;
resultObj
:
TJSObject
;
begin
response
:=
await
(
FClient
.
RawInvokeAsync
(
'ITimeEntryService.GetTaskPickerCustomers'
,
[
FUserId
]
));
resultObj
:=
TJSObject
(
response
.
Result
);
FCustomers
:=
TJSArray
(
resultObj
[
'options'
]);
FillSelect
(
'task_picker_customer'
,
FCustomers
);
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
LoadProjects
(
const
ACustomerId
:
string
);
var
response
:
TXDataClientResponse
;
resultObj
:
TJSObject
;
begin
response
:=
await
(
FClient
.
RawInvokeAsync
(
'ITimeEntryService.GetTaskPickerProjects'
,
[
FUserId
,
ACustomerId
]
));
resultObj
:=
TJSObject
(
response
.
Result
);
FProjects
:=
TJSArray
(
resultObj
[
'options'
]);
FillSelect
(
'task_picker_project'
,
FProjects
);
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
LoadTasks
(
const
AProjectId
:
string
);
var
response
:
TXDataClientResponse
;
resultObj
:
TJSObject
;
begin
response
:=
await
(
FClient
.
RawInvokeAsync
(
'ITimeEntryService.GetTaskPickerTasks'
,
[
FUserId
,
AProjectId
]
));
resultObj
:=
TJSObject
(
response
.
Result
);
FTasks
:=
TJSArray
(
resultObj
[
'options'
]);
FillSelect
(
'task_picker_task'
,
FTasks
);
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
CustomerChanged
(
Event
:
TJSEvent
);
var
customerId
:
string
;
begin
ClearStatus
;
SetSaveEnabled
(
False
);
ClearSelect
(
'task_picker_project'
,
True
);
ClearSelect
(
'task_picker_task'
,
True
);
customerId
:=
SelectedValue
(
'task_picker_customer'
);
if
customerId
=
''
then
Exit
;
try
await
(
LoadProjects
(
customerId
));
except
on
E
:
Exception
do
SetStatus
(
'Unable to load projects.'
);
end
;
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
ProjectChanged
(
Event
:
TJSEvent
);
var
projectId
:
string
;
begin
ClearStatus
;
SetSaveEnabled
(
False
);
ClearSelect
(
'task_picker_task'
,
True
);
projectId
:=
SelectedValue
(
'task_picker_project'
);
if
projectId
=
''
then
Exit
;
try
await
(
LoadTasks
(
projectId
));
except
on
E
:
Exception
do
SetStatus
(
'Unable to load tasks.'
);
end
;
end
;
procedure
TTaskPickerOffCanvas
.
TaskChanged
(
Event
:
TJSEvent
);
begin
ClearStatus
;
SetSaveEnabled
(
SelectedValue
(
'task_picker_task'
)
<>
''
);
end
;
procedure
TTaskPickerOffCanvas
.
SaveClicked
(
Event
:
TJSEvent
);
var
taskId
:
string
;
taskDisplay
:
string
;
begin
Event
.
preventDefault
;
taskId
:=
SelectedValue
(
'task_picker_task'
);
if
taskId
=
''
then
begin
SetStatus
(
'Select a task.'
);
Exit
;
end
;
taskDisplay
:=
FindDisplay
(
FTasks
,
taskId
);
if
Assigned
(
FOnTaskPicked
)
then
FOnTaskPicked
(
FRowIndex
,
taskId
,
taskDisplay
);
Hide
;
end
;
[
async
]
procedure
TTaskPickerOffCanvas
.
Open
(
const
AUserId
:
string
;
ARowIndex
:
Integer
);
begin
FUserId
:=
AUserId
;
FRowIndex
:=
ARowIndex
;
ClearStatus
;
SetSaveEnabled
(
False
);
ClearSelect
(
'task_picker_customer'
,
True
);
ClearSelect
(
'task_picker_project'
,
True
);
ClearSelect
(
'task_picker_task'
,
True
);
Show
;
try
await
(
LoadCustomers
);
except
on
E
:
Exception
do
SetStatus
(
'Unable to load customers.'
);
end
;
end
;
end
.
emT3XDataServer/Source/Api.Database.dfm
View file @
7a8e3164
...
...
@@ -6,6 +6,9 @@ object ApiDatabase: TApiDatabase
AutoCommit = False
ProviderName = 'MySQL'
Database = 'eTask'
Username = 'root'
Server = '192.168.102.131'
Connected = True
LoginPrompt = False
Left = 435
Top = 359
...
...
@@ -1149,8 +1152,14 @@ object ApiDatabase: TApiDatabase
'SET'
' TASK_DATE = :TASK_DATE,'
' TASK_ID = :TASK_ID,'
' PROJECT_ID = ('
' SELECT t.PROJECT_ID'
' FROM tasks t'
' WHERE t.TASK_ID = :TASK_ID'
' ),'
' HOURS = :HOURS,'
' TASK_TIME = :TASK_TIME,'
' PLACE = :PLACE,'
' CATEGORY = :CATEGORY,'
' SUMMARY = :SUMMARY,'
' MODIFY_DATE = now(),'
...
...
@@ -1182,6 +1191,11 @@ object ApiDatabase: TApiDatabase
end
item
DataType = ftUnknown
Name = 'PLACE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY'
Value = nil
end
...
...
@@ -1206,4 +1220,145 @@ object ApiDatabase: TApiDatabase
Value = nil
end>
end
object uqTaskPickerCustomers: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' c.CUSTOMER_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME'
'from user_project up'
'join project p on p.PROJECT_ID = up.PROJECT_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
'where up.USER_ID = :USER_ID'
'order by c.SHORT_NAME')
Left = 402
Top = 494
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
object uqTaskPickerCustomersCUSTOMER_ID: TStringField
FieldName = 'CUSTOMER_ID'
Size = 7
end
object uqTaskPickerCustomersCUSTOMER_SHORT_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME'
Size = 10
end
end
object uqTaskPickerProjects: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' p.PROJECT_ID,'
' p.NAME as PROJECT_NAME'
'from user_project up'
'join project p on p.PROJECT_ID = up.PROJECT_ID'
'where up.USER_ID = :USER_ID'
' and p.CUSTOMER_ID = :CUSTOMER_ID'
'order by p.NAME')
Left = 544
Top = 494
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'CUSTOMER_ID'
Value = nil
end>
object uqTaskPickerProjectsPROJECT_ID: TStringField
FieldName = 'PROJECT_ID'
Required = True
Size = 7
end
object uqTaskPickerProjectsPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME'
Size = 30
end
end
object uqTaskPickerTasks: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select distinct'
' t.TASK_ID,'
' c.SHORT_NAME as CUSTOMER_SHORT_NAME,'
' p.NAME as PROJECT_NAME,'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' t.SUBJECT as TASK_SUBJECT'
'from tasks t'
'join project p on p.PROJECT_ID = t.PROJECT_ID'
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID'
'join user_project up on up.PROJECT_ID = p.PROJECT_ID'
'where up.USER_ID = :USER_ID'
' and t.PROJECT_ID = :PROJECT_ID'
'order by'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' t.SUBJECT')
Left = 676
Top = 502
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'PROJECT_ID'
Value = nil
end>
object uqTaskPickerTasksTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqTaskPickerTasksCUSTOMER_SHORT_NAME: TStringField
FieldName = 'CUSTOMER_SHORT_NAME'
ReadOnly = True
Size = 10
end
object uqTaskPickerTasksPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME'
ReadOnly = True
Size = 30
end
object uqTaskPickerTasksTASK_NUM_1: TIntegerField
FieldName = 'TASK_NUM_1'
end
object uqTaskPickerTasksTASK_NUM_2: TIntegerField
FieldName = 'TASK_NUM_2'
end
object uqTaskPickerTasksTASK_NUM_3: TIntegerField
FieldName = 'TASK_NUM_3'
end
object uqTaskPickerTasksTASK_NUM_4: TIntegerField
FieldName = 'TASK_NUM_4'
end
object uqTaskPickerTasksTASK_NUM_5: TIntegerField
FieldName = 'TASK_NUM_5'
end
object uqTaskPickerTasksTASK_NUM_6: TIntegerField
FieldName = 'TASK_NUM_6'
end
object uqTaskPickerTasksTASK_SUBJECT: TStringField
FieldName = 'TASK_SUBJECT'
Size = 80
end
end
end
emT3XDataServer/Source/Api.Database.pas
View file @
7a8e3164
...
...
@@ -90,6 +90,23 @@ type
uqProjectReportedUsersNAME
:
TStringField
;
uqAddTimeEntry
:
TUniQuery
;
uqSaveTimeEntry
:
TUniQuery
;
uqTaskPickerCustomers
:
TUniQuery
;
uqTaskPickerProjects
:
TUniQuery
;
uqTaskPickerTasks
:
TUniQuery
;
uqTaskPickerCustomersCUSTOMER_ID
:
TStringField
;
uqTaskPickerCustomersCUSTOMER_SHORT_NAME
:
TStringField
;
uqTaskPickerProjectsPROJECT_ID
:
TStringField
;
uqTaskPickerProjectsPROJECT_NAME
:
TStringField
;
uqTaskPickerTasksTASK_ID
:
TStringField
;
uqTaskPickerTasksCUSTOMER_SHORT_NAME
:
TStringField
;
uqTaskPickerTasksPROJECT_NAME
:
TStringField
;
uqTaskPickerTasksTASK_NUM_1
:
TIntegerField
;
uqTaskPickerTasksTASK_NUM_2
:
TIntegerField
;
uqTaskPickerTasksTASK_NUM_3
:
TIntegerField
;
uqTaskPickerTasksTASK_NUM_4
:
TIntegerField
;
uqTaskPickerTasksTASK_NUM_5
:
TIntegerField
;
uqTaskPickerTasksTASK_NUM_6
:
TIntegerField
;
uqTaskPickerTasksTASK_SUBJECT
:
TStringField
;
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
uqUsersCalcFields
(
DataSet
:
TDataSet
);
private
...
...
emT3XDataServer/Source/TimeEntry.Service.pas
View file @
7a8e3164
...
...
@@ -20,6 +20,8 @@ type
taskDisplay
:
string
;
hours
:
Nullable
<
Double
>;
taskTime
:
string
;
place
:
string
;
placeDesc
:
string
;
category
:
string
;
categoryDesc
:
string
;
summary
:
string
;
...
...
@@ -44,6 +46,7 @@ type
items
:
TList
<
TTimeEntry
>;
taskOptions
:
TList
<
TTimeEntryTaskOption
>;
categoryOptions
:
TList
<
TTimeEntryCategoryOption
>;
placeOptions
:
TList
<
TTimeEntryCategoryOption
>;
constructor
Create
;
destructor
Destroy
;
override
;
end
;
...
...
@@ -56,16 +59,45 @@ type
taskId
:
string
;
hours
:
Double
;
taskTime
:
string
;
place
:
string
;
category
:
string
;
summary
:
string
;
end
;
TTimeEntryFieldSave
=
class
public
entryId
:
Integer
;
userId
:
string
;
fieldName
:
string
;
value
:
string
;
end
;
type
TTaskPickerOption
=
class
public
value
:
string
;
display
:
string
;
end
;
TTaskPickerOptionsResponse
=
class
public
options
:
TList
<
TTaskPickerOption
>;
constructor
Create
;
destructor
Destroy
;
override
;
end
;
[
ServiceContract
,
Model
(
API_MODEL
)]
ITimeEntryService
=
interface
(
IInvokable
)
[
'{B18BCD1E-B19A-4D25-BBA9-50A24FC4C690}'
]
[
HttpGet
]
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
[
HttpPost
]
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
[
HttpPost
]
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
[
HttpPost
]
function
SaveTimeEntryField
(
Item
:
TTimeEntryFieldSave
):
Boolean
;
[
HttpPost
]
function
DeleteTimeEntry
(
userId
:
string
;
entryId
:
Integer
):
Boolean
;
function
GetTaskPickerCustomers
(
userId
:
string
):
TTaskPickerOptionsResponse
;
function
GetTaskPickerProjects
(
userId
,
customerId
:
string
):
TTaskPickerOptionsResponse
;
function
GetTaskPickerTasks
(
userId
,
projectId
:
string
):
TTaskPickerOptionsResponse
;
end
;
implementation
...
...
@@ -76,6 +108,7 @@ begin
items
:=
TList
<
TTimeEntry
>.
Create
;
taskOptions
:=
TList
<
TTimeEntryTaskOption
>.
Create
;
categoryOptions
:=
TList
<
TTimeEntryCategoryOption
>.
Create
;
placeOptions
:=
TList
<
TTimeEntryCategoryOption
>.
Create
;
end
;
destructor
TTimeEntriesResponse
.
Destroy
;
...
...
@@ -83,6 +116,19 @@ begin
items
.
Free
;
taskOptions
.
Free
;
categoryOptions
.
Free
;
placeOptions
.
Free
;
inherited
;
end
;
constructor
TTaskPickerOptionsResponse
.
Create
;
begin
inherited
;
options
:=
TList
<
TTaskPickerOption
>.
Create
;
end
;
destructor
TTaskPickerOptionsResponse
.
Destroy
;
begin
options
.
Free
;
inherited
;
end
;
...
...
emT3XDataServer/Source/TimeEntry.ServiceImpl.pas
View file @
7a8e3164
...
...
@@ -26,12 +26,18 @@ type
procedure
LoadUserName
(
AResponse
:
TTimeEntriesResponse
;
userId
:
string
);
procedure
LoadTimeEntryItems
(
AResponse
:
TTimeEntriesResponse
;
userId
:
string
;
startDateValue
,
exclusiveEndDateValue
:
TDateTime
);
procedure
LoadTaskOptions
(
AResponse
:
TTimeEntriesResponse
;
userId
:
string
;
startDateValue
,
exclusiveEndDateValue
:
TDateTime
);
procedure
LoadPlaceOptions
(
AResponse
:
TTimeEntriesResponse
);
procedure
LoadCategoryOptions
(
AResponse
:
TTimeEntriesResponse
);
function
GetNextIdValue
(
const
AKeyName
:
string
):
string
;
public
function
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
function
AddTimeEntry
(
userId
,
taskDate
:
string
):
string
;
function
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
function
SaveTimeEntryField
(
Item
:
TTimeEntryFieldSave
):
Boolean
;
function
GetTaskPickerCustomers
(
userId
:
string
):
TTaskPickerOptionsResponse
;
function
GetTaskPickerProjects
(
userId
,
customerId
:
string
):
TTaskPickerOptionsResponse
;
function
GetTaskPickerTasks
(
userId
,
projectId
:
string
):
TTaskPickerOptionsResponse
;
function
DeleteTimeEntry
(
userId
:
string
;
entryId
:
Integer
):
Boolean
;
end
;
implementation
...
...
@@ -101,6 +107,19 @@ begin
AddPart
(
FieldText
(
AQuery
,
'TASK_NUM_6'
));
end
;
procedure
AddTaskPickerOption
(
AResponse
:
TTaskPickerOptionsResponse
;
const
AValue
,
ADisplay
:
string
);
var
option
:
TTaskPickerOption
;
begin
option
:=
TTaskPickerOption
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
option
);
option
.
value
:=
AValue
;
option
.
display
:=
ADisplay
;
AResponse
.
options
.
Add
(
option
);
end
;
function
TTimeEntryService
.
BuildTaskDisplay
(
AQuery
:
TUniQuery
):
string
;
var
...
...
@@ -187,6 +206,7 @@ begin
' ti.TASK_ID, '
+
' ti.HOURS, '
+
' ti.TASK_TIME, '
+
' ti.PLACE, '
+
' ti.CATEGORY, '
+
' cat.CODE_DESC as CATEGORY_DESC, '
+
' ti.SUMMARY, '
+
...
...
@@ -198,12 +218,14 @@ begin
' t.TASK_NUM_4, '
+
' t.TASK_NUM_5, '
+
' t.TASK_NUM_6, '
+
' t.SUBJECT as TASK_SUBJECT '
+
' t.SUBJECT as TASK_SUBJECT, '
+
' plc.CODE_DESC as PLACE_DESC '
+
'from time_items ti '
+
'left join tasks t on t.TASK_ID = ti.TASK_ID '
+
'left join project p on p.PROJECT_ID = t.PROJECT_ID '
+
'left join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID '
+
'left join codes cat on cat.CATEGORY = ''TASK'' and cat.CODE_TYPE = ''CATGRY'' and cat.CODE = ti.CATEGORY '
+
'left join codes plc on plc.CATEGORY = ''GENERAL'' and plc.CODE_TYPE = ''PLACE'' and plc.CODE = ti.PLACE '
+
'where ti.USER_ID = :USER_ID '
+
' and ti.TASK_DATE >= :START_DATE '
+
' and ti.TASK_DATE < :END_DATE '
+
...
...
@@ -234,6 +256,12 @@ begin
item
.
taskTime
:=
FieldText
(
itemsQuery
,
'TASK_TIME'
);
item
.
place
:=
FieldText
(
itemsQuery
,
'PLACE'
);
item
.
placeDesc
:=
FieldText
(
itemsQuery
,
'PLACE_DESC'
);
if
item
.
placeDesc
=
''
then
item
.
placeDesc
:=
item
.
place
;
item
.
category
:=
FieldText
(
itemsQuery
,
'CATEGORY'
);
item
.
categoryDesc
:=
FieldText
(
itemsQuery
,
'CATEGORY_DESC'
);
...
...
@@ -271,32 +299,15 @@ begin
' t.TASK_NUM_5, '
+
' t.TASK_NUM_6, '
+
' t.SUBJECT as TASK_SUBJECT '
+
'from tasks t '
+
'from task_assigned_user tau '
+
'join tasks t on t.TASK_ID = tau.TASK_ID '
+
'join project p on p.PROJECT_ID = t.PROJECT_ID '
+
'join customers c on c.CUSTOMER_ID = p.CUSTOMER_ID '
+
'where ( '
+
' ( '
+
' t.ASSIGNED_TO = :USER_ID '
+
' and t.FOCUS = ''T'' '
+
' and (t.START_DATE <= :ACTIVE_DATE or t.START_DATE is null) '
+
' and (t.COMPLETION_DATE >= :ACTIVE_DATE or t.COMPLETION_DATE is null) '
+
' ) '
+
' or t.TASK_ID in ( '
+
' select distinct ti.TASK_ID '
+
' from time_items ti '
+
' where ti.USER_ID = :USER_ID_EXISTING '
+
' and ti.TASK_DATE >= :START_DATE '
+
' and ti.TASK_DATE < :END_DATE '
+
' and ti.TASK_ID is not null '
+
' ) '
+
') '
+
'where tau.USER_ID = :USER_ID '
+
' and upper(trim(coalesce(tau.FOCUSED_TASK, ''''))) = ''T'' '
+
'order by c.SHORT_NAME, p.NAME, t.TASK_NUM_1, t.TASK_NUM_2, t.TASK_NUM_3, t.TASK_NUM_4, t.TASK_NUM_5, t.TASK_NUM_6'
;
tasksQuery
.
ParamByName
(
'USER_ID'
).
AsString
:=
userId
;
tasksQuery
.
ParamByName
(
'USER_ID_EXISTING'
).
AsString
:=
userId
;
tasksQuery
.
ParamByName
(
'ACTIVE_DATE'
).
AsDateTime
:=
startDateValue
;
tasksQuery
.
ParamByName
(
'START_DATE'
).
AsDateTime
:=
startDateValue
;
tasksQuery
.
ParamByName
(
'END_DATE'
).
AsDateTime
:=
exclusiveEndDateValue
;
tasksQuery
.
Open
;
while
not
tasksQuery
.
Eof
do
...
...
@@ -350,6 +361,40 @@ begin
end
;
procedure
TTimeEntryService
.
LoadPlaceOptions
(
AResponse
:
TTimeEntriesResponse
);
var
placeQuery
:
TUniQuery
;
placeOption
:
TTimeEntryCategoryOption
;
begin
placeQuery
:=
TUniQuery
.
Create
(
nil
);
try
placeQuery
.
Connection
:=
apiDB
.
ucETaskApi
;
placeQuery
.
SQL
.
Text
:=
'select CODE, CODE_DESC '
+
'from codes '
+
'where CATEGORY = ''GENERAL'' '
+
' and CODE_TYPE = ''PLACE'' '
+
'order by CODE_DESC'
;
placeQuery
.
Open
;
while
not
placeQuery
.
Eof
do
begin
placeOption
:=
TTimeEntryCategoryOption
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
placeOption
);
placeOption
.
code
:=
FieldText
(
placeQuery
,
'CODE'
);
placeOption
.
codeDesc
:=
FieldText
(
placeQuery
,
'CODE_DESC'
);
AResponse
.
placeOptions
.
Add
(
placeOption
);
placeQuery
.
Next
;
end
;
finally
placeQuery
.
Free
;
end
;
end
;
function
TTimeEntryService
.
GetTimeEntries
(
userId
,
startDate
,
endDate
:
string
):
TTimeEntriesResponse
;
var
startDateValue
:
TDateTime
;
...
...
@@ -368,6 +413,7 @@ begin
LoadUserName
(
Result
,
userId
);
LoadTimeEntryItems
(
Result
,
userId
,
startDateValue
,
exclusiveEndDateValue
);
LoadTaskOptions
(
Result
,
userId
,
startDateValue
,
exclusiveEndDateValue
);
LoadPlaceOptions
(
Result
);
LoadCategoryOptions
(
Result
);
Result
.
count
:=
Result
.
items
.
Count
;
...
...
@@ -433,13 +479,31 @@ end;
function
TTimeEntryService
.
SaveTimeEntry
(
Item
:
TTimeEntrySave
):
Boolean
;
var
taskDateValue
:
TDateTime
;
entryId
:
string
;
isNewEntry
:
Boolean
;
begin
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.SaveTimeEntry - ENTRY_ID="%s" USER_ID="%s"'
,
[
Item
.
entryId
,
Item
.
userId
]));
taskDateValue
:=
ParseIsoDate
(
Item
.
taskDate
);
isNewEntry
:=
StrToIntDef
(
Item
.
entryId
,
0
)
<=
0
;
if
isNewEntry
then
begin
entryId
:=
GetNextIdValue
(
'TimeEntryId'
);
apiDB
.
uqAddTimeEntry
.
Close
;
apiDB
.
uqAddTimeEntry
.
ParamByName
(
'ENTRY_ID'
).
AsString
:=
entryId
;
apiDB
.
uqAddTimeEntry
.
ParamByName
(
'USER_ID'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqAddTimeEntry
.
ParamByName
(
'TASK_DATE'
).
AsDateTime
:=
taskDateValue
;
apiDB
.
uqAddTimeEntry
.
ParamByName
(
'CREATED_BY'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqAddTimeEntry
.
ParamByName
(
'MODIFIED_BY'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqAddTimeEntry
.
ExecSQL
;
end
else
entryId
:=
Item
.
entryId
;
apiDB
.
uqSaveTimeEntry
.
Close
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'ENTRY_ID'
).
AsString
:=
Item
.
entryId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'ENTRY_ID'
).
AsString
:=
entryId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'USER_ID'
).
AsString
:=
Item
.
userId
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_DATE'
).
AsDateTime
:=
taskDateValue
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_ID'
).
AsString
:=
Item
.
taskId
;
...
...
@@ -450,6 +514,11 @@ begin
else
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'TASK_TIME'
).
AsString
:=
Item
.
taskTime
;
if
Trim
(
Item
.
place
)
=
''
then
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'PLACE'
).
Clear
else
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'PLACE'
).
AsString
:=
Item
.
place
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'CATEGORY'
).
AsString
:=
Item
.
category
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'SUMMARY'
).
AsString
:=
Item
.
summary
;
apiDB
.
uqSaveTimeEntry
.
ParamByName
(
'MODIFIED_BY'
).
AsString
:=
Item
.
userId
;
...
...
@@ -458,7 +527,286 @@ begin
Result
:=
True
;
Logger
.
Log
(
4
,
'TimeEntryService.SaveTimeEntry - saved ENTRY_ID='
+
Item
.
entryId
);
Logger
.
Log
(
4
,
'TimeEntryService.SaveTimeEntry - saved ENTRY_ID='
+
entryId
);
end
;
function
TTimeEntryService
.
SaveTimeEntryField
(
Item
:
TTimeEntryFieldSave
):
Boolean
;
var
uqSaveField
:
TUniQuery
;
columnName
:
string
;
d
:
TDateTime
;
function
MapFieldNameToColumn
(
AFieldName
:
string
):
string
;
begin
Result
:=
''
;
if
SameText
(
AFieldName
,
'taskDate'
)
then
Result
:=
'TASK_DATE'
else
if
SameText
(
AFieldName
,
'hours'
)
then
Result
:=
'HOURS'
else
if
SameText
(
AFieldName
,
'taskTime'
)
then
Result
:=
'TASK_TIME'
else
if
SameText
(
AFieldName
,
'place'
)
then
Result
:=
'PLACE'
else
if
SameText
(
AFieldName
,
'category'
)
then
Result
:=
'CATEGORY'
else
if
SameText
(
AFieldName
,
'summary'
)
then
Result
:=
'SUMMARY'
;
end
;
function
ParseDateOrZero
(
const
S
:
string
;
out
ADate
:
TDateTime
):
Boolean
;
var
y
:
Integer
;
m
:
Integer
;
dInt
:
Integer
;
begin
Result
:=
False
;
ADate
:=
0
;
if
Length
(
Trim
(
S
))
<>
10
then
Exit
;
y
:=
StrToIntDef
(
Copy
(
S
,
1
,
4
),
0
);
m
:=
StrToIntDef
(
Copy
(
S
,
6
,
2
),
0
);
dInt
:=
StrToIntDef
(
Copy
(
S
,
9
,
2
),
0
);
if
(
y
>
0
)
and
(
m
>
0
)
and
(
dInt
>
0
)
then
begin
try
ADate
:=
EncodeDate
(
y
,
m
,
dInt
);
Result
:=
True
;
except
Result
:=
False
;
end
;
end
;
end
;
begin
Result
:=
False
;
if
not
Assigned
(
Item
)
then
raise
Exception
.
Create
(
'SaveTimeEntryField: Item is nil.'
);
if
Item
.
entryId
<=
0
then
raise
Exception
.
Create
(
'SaveTimeEntryField: Invalid entryId.'
);
if
Trim
(
Item
.
userId
)
=
''
then
raise
Exception
.
Create
(
'SaveTimeEntryField: Invalid userId.'
);
uqSaveField
:=
TUniQuery
.
Create
(
nil
);
try
uqSaveField
.
Connection
:=
apiDB
.
ucETaskApi
;
if
SameText
(
Item
.
fieldName
,
'taskId'
)
then
begin
if
Trim
(
Item
.
value
)
=
''
then
begin
uqSaveField
.
SQL
.
Text
:=
'update time_items '
+
'set TASK_ID = null, '
+
' PROJECT_ID = null, '
+
' MODIFY_DATE = now(), '
+
' MODIFIED_BY = :MODIFIED_BY '
+
'where ENTRY_ID = :ENTRY_ID '
+
' and USER_ID = :USER_ID'
;
end
else
begin
uqSaveField
.
SQL
.
Text
:=
'update time_items '
+
'set TASK_ID = :TASK_ID, '
+
' PROJECT_ID = ( '
+
' select t.PROJECT_ID '
+
' from tasks t '
+
' where t.TASK_ID = :TASK_ID '
+
' ), '
+
' MODIFY_DATE = now(), '
+
' MODIFIED_BY = :MODIFIED_BY '
+
'where ENTRY_ID = :ENTRY_ID '
+
' and USER_ID = :USER_ID'
;
uqSaveField
.
ParamByName
(
'TASK_ID'
).
AsString
:=
Item
.
value
;
end
;
end
else
begin
columnName
:=
MapFieldNameToColumn
(
Item
.
fieldName
);
if
columnName
=
''
then
raise
Exception
.
Create
(
'SaveTimeEntryField: Invalid field name: '
+
Item
.
fieldName
);
uqSaveField
.
SQL
.
Text
:=
'update time_items '
+
'set '
+
columnName
+
' = :'
+
columnName
+
', '
+
' MODIFY_DATE = now(), '
+
' MODIFIED_BY = :MODIFIED_BY '
+
'where ENTRY_ID = :ENTRY_ID '
+
' and USER_ID = :USER_ID'
;
if
SameText
(
Item
.
fieldName
,
'taskDate'
)
then
begin
if
ParseDateOrZero
(
Item
.
value
,
d
)
then
uqSaveField
.
ParamByName
(
columnName
).
AsDateTime
:=
d
else
uqSaveField
.
ParamByName
(
columnName
).
Clear
;
end
else
if
SameText
(
Item
.
fieldName
,
'hours'
)
then
begin
if
Trim
(
Item
.
value
)
=
''
then
uqSaveField
.
ParamByName
(
columnName
).
Clear
else
uqSaveField
.
ParamByName
(
columnName
).
AsFloat
:=
StrToFloatDef
(
Item
.
value
,
0
);
end
else
begin
if
Trim
(
Item
.
value
)
=
''
then
uqSaveField
.
ParamByName
(
columnName
).
Clear
else
uqSaveField
.
ParamByName
(
columnName
).
AsString
:=
Item
.
value
;
end
;
end
;
uqSaveField
.
ParamByName
(
'ENTRY_ID'
).
AsInteger
:=
Item
.
entryId
;
uqSaveField
.
ParamByName
(
'USER_ID'
).
AsString
:=
Item
.
userId
;
uqSaveField
.
ParamByName
(
'MODIFIED_BY'
).
AsString
:=
Item
.
userId
;
uqSaveField
.
ExecSQL
;
Result
:=
True
;
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.SaveTimeEntryField - ENTRY_ID="%d" FIELD="%s"'
,
[
Item
.
entryId
,
Item
.
fieldName
]
));
finally
uqSaveField
.
Free
;
end
;
end
;
function
TTimeEntryService
.
GetTaskPickerCustomers
(
userId
:
string
):
TTaskPickerOptionsResponse
;
begin
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.GetTaskPickerCustomers - USER_ID="%s"'
,
[
userId
]));
Result
:=
TTaskPickerOptionsResponse
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
apiDB
.
uqTaskPickerCustomers
.
Close
;
apiDB
.
uqTaskPickerCustomers
.
ParamByName
(
'USER_ID'
).
AsString
:=
userId
;
apiDB
.
uqTaskPickerCustomers
.
Open
;
try
while
not
apiDB
.
uqTaskPickerCustomers
.
Eof
do
begin
AddTaskPickerOption
(
Result
,
FieldText
(
apiDB
.
uqTaskPickerCustomers
,
'CUSTOMER_ID'
),
FieldText
(
apiDB
.
uqTaskPickerCustomers
,
'CUSTOMER_SHORT_NAME'
)
);
apiDB
.
uqTaskPickerCustomers
.
Next
;
end
;
finally
apiDB
.
uqTaskPickerCustomers
.
Close
;
end
;
end
;
function
TTimeEntryService
.
GetTaskPickerProjects
(
userId
,
customerId
:
string
):
TTaskPickerOptionsResponse
;
begin
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.GetTaskPickerProjects - USER_ID="%s" CUSTOMER_ID="%s"'
,
[
userId
,
customerId
]));
Result
:=
TTaskPickerOptionsResponse
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
apiDB
.
uqTaskPickerProjects
.
Close
;
apiDB
.
uqTaskPickerProjects
.
ParamByName
(
'USER_ID'
).
AsString
:=
userId
;
apiDB
.
uqTaskPickerProjects
.
ParamByName
(
'CUSTOMER_ID'
).
AsString
:=
customerId
;
apiDB
.
uqTaskPickerProjects
.
Open
;
try
while
not
apiDB
.
uqTaskPickerProjects
.
Eof
do
begin
AddTaskPickerOption
(
Result
,
FieldText
(
apiDB
.
uqTaskPickerProjects
,
'PROJECT_ID'
),
FieldText
(
apiDB
.
uqTaskPickerProjects
,
'PROJECT_NAME'
)
);
apiDB
.
uqTaskPickerProjects
.
Next
;
end
;
finally
apiDB
.
uqTaskPickerProjects
.
Close
;
end
;
end
;
function
TTimeEntryService
.
GetTaskPickerTasks
(
userId
,
projectId
:
string
):
TTaskPickerOptionsResponse
;
begin
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.GetTaskPickerTasks - USER_ID="%s" PROJECT_ID="%s"'
,
[
userId
,
projectId
]));
Result
:=
TTaskPickerOptionsResponse
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
apiDB
.
uqTaskPickerTasks
.
Close
;
apiDB
.
uqTaskPickerTasks
.
ParamByName
(
'USER_ID'
).
AsString
:=
userId
;
apiDB
.
uqTaskPickerTasks
.
ParamByName
(
'PROJECT_ID'
).
AsString
:=
projectId
;
apiDB
.
uqTaskPickerTasks
.
Open
;
try
while
not
apiDB
.
uqTaskPickerTasks
.
Eof
do
begin
AddTaskPickerOption
(
Result
,
FieldText
(
apiDB
.
uqTaskPickerTasks
,
'TASK_ID'
),
BuildTaskDisplay
(
apiDB
.
uqTaskPickerTasks
)
);
apiDB
.
uqTaskPickerTasks
.
Next
;
end
;
finally
apiDB
.
uqTaskPickerTasks
.
Close
;
end
;
end
;
function
TTimeEntryService
.
DeleteTimeEntry
(
userId
:
string
;
entryId
:
Integer
):
Boolean
;
var
q
:
TUniQuery
;
begin
Result
:=
False
;
if
Trim
(
userId
)
=
''
then
raise
Exception
.
Create
(
'DeleteTimeEntry: Invalid userId.'
);
if
entryId
<=
0
then
raise
Exception
.
Create
(
'DeleteTimeEntry: Invalid entryId.'
);
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.DeleteTimeEntry - USER_ID="%s" ENTRY_ID="%d"'
,
[
userId
,
entryId
]
));
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
apiDB
.
ucETaskApi
;
q
.
SQL
.
Text
:=
'delete from time_items '
+
'where ENTRY_ID = :ENTRY_ID '
+
' and USER_ID = :USER_ID'
;
q
.
ParamByName
(
'ENTRY_ID'
).
AsInteger
:=
entryId
;
q
.
ParamByName
(
'USER_ID'
).
AsString
:=
userId
;
q
.
ExecSQL
;
if
q
.
RowsAffected
<=
0
then
raise
Exception
.
Create
(
'Time entry was not found or does not belong to this user.'
);
Result
:=
True
;
Logger
.
Log
(
4
,
Format
(
'TimeEntryService.DeleteTimeEntry - deleted ENTRY_ID="%d"'
,
[
entryId
]
));
finally
q
.
Free
;
end
;
end
;
...
...
emT3XDataServer/bin/emT3XDataServer.ini
View file @
7a8e3164
...
...
@@ -2,7 +2,7 @@
MemoLogLevel
=
4
FileLogLevel
=
4
webClientVersion
=
0.8.9
LogFileNum
=
2
22
LogFileNum
=
2
36
[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