Commit 41b5088d by Mac Stephens

Add server-backed task row insertion with next item_num, and remove old…

Add server-backed task row insertion with next item_num, and remove old blank-row auto-create behavior. Implement row reordering by editable item_num, including transactional shifting logic and integer task_item_id cleanup across server/client.
parent 07dfa883
......@@ -22,3 +22,9 @@ emT3Web/__recovery/
emT3WebApp/__history/
*.zip
emT3WebApp/css/__history/
emT3XDataServer/bin/static/
emT3WebApp/Win32/Debug/
......@@ -71,7 +71,7 @@ object FTasksHTML: TFTasksHTML
object xdwdsTasksnotes: TStringField
FieldName = 'notes'
end
object xdwdsTaskstaskItemId: TStringField
object xdwdsTaskstaskItemId: TIntegerField
FieldName = 'taskItemId'
end
end
......
......@@ -27,8 +27,8 @@ type
xdwdsTasksnotes: TStringField;
btnReload: TWebButton;
btnAddRow: TWebButton;
xdwdsTaskstaskItemId: TStringField;
xdwdsTasksitemNum: TIntegerField;
xdwdsTaskstaskItemId: TIntegerField;
[async] procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
......@@ -57,6 +57,7 @@ type
procedure DropdownItemClick(Event: TJSEvent);
procedure DropdownEditClick(Event: TJSEvent);
function ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
[async] procedure MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
public
end;
......@@ -173,6 +174,9 @@ begin
console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal);
xdwdsTasks.Edit;
if SameText(fieldName, 'itemNum') then
xdwdsTasks.FieldByName(fieldName).AsInteger := StrToIntDef(newVal, 0)
else
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
xdwdsTasks.Post;
......@@ -382,6 +386,8 @@ begin
xdwdsTasks.SetJsonData(itemsArray);
xdwdsTasks.Open;
btnAddRow.Enabled := True;
RenderTable;
finally
Utils.HideSpinner('spinner');
......@@ -433,16 +439,18 @@ var
begin
Result :=
'<textarea class="form-control form-control-sm cell-textarea task-editor w-100" ' +
'style="height:31px; min-height:31px; overflow:hidden; resize:none;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'rows="2">' + HtmlEncode(Value) + '</textarea>';
'rows="1">' + HtmlEncode(Value) + '</textarea>';
end;
function RowNumCell(const Value: Integer): string;
function ItemNumInput(const Value: Integer; const AIdx: Integer): string;
begin
Result :=
'<div class="form-control form-control-sm text-center bg-light-subtle">' +
IntToStr(Value) +
'</div>';
'<input type="number" min="1" class="form-control form-control-sm task-editor text-center px-1" ' +
'style="width: 30px;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="itemNum" ' +
'value="' + IntToStr(Value) + '">';
end;
function DateInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
......@@ -541,7 +549,7 @@ begin
html :=
'<div class="tasks-vscroll">' +
'<div class="tasks-hscroll">' +
'<table class="table table-sm table-bordered table-hover align-middle mb-0">' +
'<table class="table table-sm table-bordered align-middle mb-0">' +
'<colgroup>' +
'<col style="width:40px">' +
'<col style="width:240px">' +
......@@ -575,7 +583,7 @@ begin
begin
html := html +
'<tr>' +
TdNowrap(RowNumCell(xdwdsTasksitemNum.AsInteger)) +
TdNowrap(ItemNumInput(xdwdsTasksitemNum.AsInteger, rowIdx)) +
TdNowrap(TextInput('application', xdwdsTasksapplication.AsString, rowIdx, 180)) +
TdNowrap(TextInput('version', xdwdsTasksversion.AsString, rowIdx, 80)) +
TdNowrap(DateInput('taskDate', xdwdsTaskstaskDate.AsString, rowIdx, 110)) +
......@@ -645,6 +653,7 @@ var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName: string;
newItemNum: Integer;
begin
el := TJSHTMLElement(Event.target);
......@@ -663,12 +672,52 @@ begin
if idx < 0 then
Exit;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
if SameText(fieldName, 'itemNum') then
begin
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
console.log('EditorBlur: MOVE idx=' + IntToStr(idx) + ' newItemNum=' + IntToStr(newItemNum));
MoveTaskRow(idx, newItemNum);
Exit;
end;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
SaveRow(idx);
end;
[async] procedure TFTasksHTML.MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
var
response: TXDataClientResponse;
begin
if not xdwdsTasks.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTasks.Eof then
Exit;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow',
[
StrToIntDef(xdwdsTaskstaskId.AsString, 0),
xdwdsTaskstaskItemId.AsInteger,
newItemNum
]
));
console.log('MoveTaskRow: response=' + string(TJSJSON.stringify(response.Result)));
LoadTasks(FTaskId);
except
on E: EXDataClientRequestException do
begin
console.log('MoveTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
LoadTasks(FTaskId);
end;
end;
end;
[async] procedure TFTasksHTML.SaveRow(AIndex: Integer);
const
// Note: Use this to manipulate saving to the server or not for testing
......@@ -687,7 +736,7 @@ begin
payload := TJSObject.new;
payload['taskItemId'] := xdwdsTaskstaskItemId.AsString;
payload['taskItemId'] := xdwdsTaskstaskItemId.AsInteger;
payload['taskId'] := xdwdsTaskstaskId.AsString;
payload['application'] := xdwdsTasksapplication.AsString;
......
[Paths]
HtmlPath=C:\Projects\emT3web\emT3XDataServer\bin\static
HtmlFile=index.html
DefaultURL=http://localhost:8000/emT3WebApp
SingleInstance=0
Debug=0
DebugManager=C:\RADTools\TMS\Products\tms.webcore\Bin\Win32\TMSDBGManager.exe
URL=http://localhost:8000/$(ProjectName)
URLParams=?user_id=1019&task_id=4000&url_code=123456
Browser=1
BrowserBin=
BrowserParams=
Electron=0
ElectronBuild=0
JSDebugger=1
......@@ -39,5 +39,16 @@ is-invalid .form-check-input {
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{DB6F5DBF-7E4B-45DA-AFFA-6C8DF15BA740}</ProjectGuid>
<ProjectVersion>20.3</ProjectVersion>
<ProjectVersion>20.4</ProjectVersion>
<FrameworkType>VCL</FrameworkType>
<MainSource>emT3WebApp.dpr</MainSource>
<Base>True</Base>
......@@ -907,6 +907,9 @@
<Platform Name="Win64x">
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceDebug">
<Platform Name="iOSDevice32">
......@@ -977,6 +980,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo44">
<Platform Name="Win32">
......@@ -987,6 +994,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iOS_AppStore1024">
<Platform Name="iOSDevice64">
......@@ -1201,6 +1212,7 @@
<ProjectRoot Platform="Win32" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64x" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="WinARM64EC" Name="$(PROJECTNAME)"/>
</Deployment>
<Platforms>
<Platform value="Win32">True</Platform>
......
......@@ -10,21 +10,21 @@ object ApiDatabase: TApiDatabase
Server = '192.168.102.131'
Connected = True
LoginPrompt = False
Left = 61
Top = 71
Left = 25
Top = 13
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 170
Top = 72
Left = 56
Top = 14
end
object uqUsers: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'SELECT USER_ID, NAME, STATUS from users ORDER BY NAME')
OnCalcFields = uqUsersCalcFields
Left = 420
Top = 28
Left = 414
Top = 40
object uqUsersUSER_ID: TIntegerField
FieldName = 'USER_ID'
Required = True
......@@ -60,8 +60,8 @@ object ApiDatabase: TApiDatabase
' ISSUE = :ISSUE,'
' NOTES = :NOTES'
'WHERE TASK_ITEM_ID = :TASK_ITEM_ID')
Left = 424
Top = 138
Left = 408
Top = 242
ParamData = <
item
DataType = ftUnknown
......@@ -145,18 +145,16 @@ object ApiDatabase: TApiDatabase
'from task_items'
'where TASK_ID = :TASK_ID'
'order by ITEM_NUM')
Left = 422
Top = 84
Left = 62
Top = 138
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqTaskItemsTASK_ITEM_ID: TStringField
object uqTaskItemsTASK_ITEM_ID: TIntegerField
FieldName = 'TASK_ITEM_ID'
Required = True
Size = 7
end
object uqTaskItemsTASK_ID: TStringField
FieldName = 'TASK_ID'
......@@ -221,14 +219,12 @@ object ApiDatabase: TApiDatabase
Size = 1000
end
end
object uqEnsureBlankRow: TUniQuery
object uqAddTaskRow: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert ignore into task_items ('
' TASK_ITEM_ID,'
'insert into task_items ('
' TASK_ID,'
' ITEM_NUM,'
' PROJECT_ID,'
' APPLICATION,'
' APP_VERSION,'
' TASK_DATE,'
......@@ -243,10 +239,11 @@ object ApiDatabase: TApiDatabase
')'
'values ('
' :TASK_ID,'
' :TASK_ID,'
' coalesce((select PROJECT_ID from tasks where TASK_ID = :TASK_I' +
'D), '#39#39'),'
' ('
' select coalesce(max(ti.ITEM_NUM), 0) + 1'
' from task_items ti'
' where ti.TASK_ID = :TASK_ID'
' ),'
' '#39#39','
' '#39#39','
' curdate(),'
......@@ -259,8 +256,8 @@ object ApiDatabase: TApiDatabase
' '#39#39','
' '#39#39
')')
Left = 426
Top = 192
Left = 412
Top = 300
ParamData = <
item
DataType = ftUnknown
......@@ -287,8 +284,8 @@ object ApiDatabase: TApiDatabase
'left join project p'
' on p.PROJECT_ID = t.PROJECT_ID'
'where t.TASK_ID = :TASK_ID')
Left = 172
Top = 240
Left = 60
Top = 242
ParamData = <
item
DataType = ftUnknown
......@@ -336,114 +333,6 @@ object ApiDatabase: TApiDatabase
Size = 30
end
end
object uqTaskItemsForParent: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' ti.TASK_ITEM_ID,'
' ti.TASK_ID,'
' ti.ITEM_NUM,'
' ti.APPLICATION,'
' ti.APP_VERSION,'
' ti.TASK_DATE,'
' ti.STATUS_DATE,'
' ti.REPORTED_BY,'
' ti.ASSIGNED_TO,'
' ti.STATUS,'
' ti.FIXED_VERSION,'
' ti.FORM_SECTION,'
' ti.ISSUE,'
' ti.NOTES'
'from task_items ti'
'join tasks t'
' on t.TASK_ID = ti.TASK_ID'
'where ti.TASK_ID = :TASK_ID'
' or t.PARENT_ID = :TASK_ID'
'order by'
' case when ti.TASK_ID = :TASK_ID then 0 else 1 end,'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' ti.ITEM_NUM')
Left = 172
Top = 188
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqTaskItemsForParentTASK_ITEM_ID: TStringField
FieldName = 'TASK_ITEM_ID'
Required = True
Size = 7
end
object uqTaskItemsForParentTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqTaskItemsForParentITEM_NUM: TSmallintField
FieldName = 'ITEM_NUM'
Required = True
end
object uqTaskItemsForParentAPPLICATION: TStringField
FieldName = 'APPLICATION'
Required = True
Size = 255
end
object uqTaskItemsForParentAPP_VERSION: TStringField
FieldName = 'APP_VERSION'
Required = True
Size = 50
end
object uqTaskItemsForParentTASK_DATE: TDateField
FieldName = 'TASK_DATE'
Required = True
end
object uqTaskItemsForParentSTATUS_DATE: TDateField
FieldName = 'STATUS_DATE'
Required = True
end
object uqTaskItemsForParentREPORTED_BY: TStringField
FieldName = 'REPORTED_BY'
Required = True
Size = 50
end
object uqTaskItemsForParentASSIGNED_TO: TStringField
FieldName = 'ASSIGNED_TO'
Required = True
Size = 50
end
object uqTaskItemsForParentSTATUS: TStringField
FieldName = 'STATUS'
Required = True
Size = 100
end
object uqTaskItemsForParentFIXED_VERSION: TStringField
FieldName = 'FIXED_VERSION'
Required = True
Size = 50
end
object uqTaskItemsForParentFORM_SECTION: TStringField
FieldName = 'FORM_SECTION'
Required = True
Size = 255
end
object uqTaskItemsForParentISSUE: TStringField
FieldName = 'ISSUE'
Required = True
Size = 1000
end
object uqTaskItemsForParentNOTES: TStringField
FieldName = 'NOTES'
Required = True
Size = 1000
end
end
object uqTaskItemUsers: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
......@@ -455,8 +344,8 @@ object ApiDatabase: TApiDatabase
'from task_item_user'
'where TASK_ID = :TASK_ID'
'order by USER_TYPE, NAME')
Left = 170
Top = 134
Left = 62
Top = 188
ParamData = <
item
DataType = ftUnknown
......@@ -494,9 +383,8 @@ object ApiDatabase: TApiDatabase
'FROM etask.task_item_codes'
'WHERE CODE_TYPE = '#39'STATUS'#39
'ORDER BY CODE')
Active = True
Left = 428
Top = 248
Left = 60
Top = 296
object uqTaskItemCodesCODE: TStringField
FieldName = 'CODE'
Required = True
......@@ -511,4 +399,168 @@ object ApiDatabase: TApiDatabase
Size = 50
end
end
object uqGetTaskRowPosition: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' TASK_ITEM_ID,'
' TASK_ID,'
' ITEM_NUM'
'from task_items'
'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID')
Active = True
Left = 240
Top = 16
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ITEM_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqGetTaskRowPositionTASK_ITEM_ID: TIntegerField
AutoGenerateValue = arAutoInc
FieldName = 'TASK_ITEM_ID'
end
object uqGetTaskRowPositionTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqGetTaskRowPositionITEM_NUM: TSmallintField
FieldName = 'ITEM_NUM'
Required = True
end
end
object uqMoveTaskRowTemp: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'update task_items'
'set ITEM_NUM = :TEMP_ITEM_NUM'
'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID')
Left = 238
Top = 72
ParamData = <
item
DataType = ftUnknown
Name = 'TEMP_ITEM_NUM'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ITEM_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
end
object uqShiftTaskRowsDown: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'update task_items'
'set ITEM_NUM = ITEM_NUM + 1'
'where TASK_ID = :TASK_ID'
' and ITEM_NUM >= :NEW_ITEM_NUM'
' and ITEM_NUM < :OLD_ITEM_NUM')
Left = 234
Top = 132
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'NEW_ITEM_NUM'
Value = nil
end
item
DataType = ftUnknown
Name = 'OLD_ITEM_NUM'
Value = nil
end>
end
object uqShiftTaskRowsUp: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'update task_items'
'set ITEM_NUM = ITEM_NUM - 1'
'where TASK_ID = :TASK_ID'
' and ITEM_NUM > :OLD_ITEM_NUM'
' and ITEM_NUM <= :NEW_ITEM_NUM')
Left = 236
Top = 184
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'OLD_ITEM_NUM'
Value = nil
end
item
DataType = ftUnknown
Name = 'NEW_ITEM_NUM'
Value = nil
end>
end
object uqSetTaskRowFinalPosition: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'update task_items'
'set ITEM_NUM = :NEW_ITEM_NUM'
'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID')
Left = 234
Top = 232
ParamData = <
item
DataType = ftUnknown
Name = 'NEW_ITEM_NUM'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ITEM_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
end
object uqGetTaskMaxItemNum: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' coalesce(max(ITEM_NUM), 0) as MAX_ITEM_NUM'
'from task_items'
'where TASK_ID = :TASK_ID')
Left = 236
Top = 290
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqGetTaskMaxItemNumMAX_ITEM_NUM: TIntegerField
FieldName = 'MAX_ITEM_NUM'
ReadOnly = True
end
end
end
......@@ -18,8 +18,7 @@ type
uqUsersREPRESENTATIVE: TStringField;
uqSaveTaskRow: TUniQuery;
uqTaskItems: TUniQuery;
uqEnsureBlankRow: TUniQuery;
uqTaskItemsTASK_ITEM_ID: TStringField;
uqAddTaskRow: TUniQuery;
uqTaskItemsTASK_ID: TStringField;
uqTaskItemsITEM_NUM: TShortintField;
uqTaskItemsAPPLICATION: TStringField;
......@@ -45,21 +44,6 @@ type
uqTaskHeaderPROJECT_ID: TStringField;
uqTaskHeaderSUBJECT: TStringField;
uqTaskHeaderPROJECT_NAME: TStringField;
uqTaskItemsForParent: TUniQuery;
uqTaskItemsForParentTASK_ITEM_ID: TStringField;
uqTaskItemsForParentTASK_ID: TStringField;
uqTaskItemsForParentITEM_NUM: TSmallintField;
uqTaskItemsForParentAPPLICATION: TStringField;
uqTaskItemsForParentAPP_VERSION: TStringField;
uqTaskItemsForParentTASK_DATE: TDateField;
uqTaskItemsForParentSTATUS_DATE: TDateField;
uqTaskItemsForParentREPORTED_BY: TStringField;
uqTaskItemsForParentASSIGNED_TO: TStringField;
uqTaskItemsForParentSTATUS: TStringField;
uqTaskItemsForParentFIXED_VERSION: TStringField;
uqTaskItemsForParentFORM_SECTION: TStringField;
uqTaskItemsForParentISSUE: TStringField;
uqTaskItemsForParentNOTES: TStringField;
uqTaskItemUsers: TUniQuery;
uqTaskItemUsersTASK_ITEM_USER_ID: TStringField;
uqTaskItemUsersTASK_ID: TStringField;
......@@ -69,6 +53,17 @@ type
uqTaskItemCodesCODE: TStringField;
uqTaskItemCodesCODE_DESC: TStringField;
uqTaskItemCodesCODE_TYPE: TStringField;
uqGetTaskRowPosition: TUniQuery;
uqMoveTaskRowTemp: TUniQuery;
uqShiftTaskRowsDown: TUniQuery;
uqShiftTaskRowsUp: TUniQuery;
uqSetTaskRowFinalPosition: TUniQuery;
uqGetTaskMaxItemNum: TUniQuery;
uqGetTaskRowPositionTASK_ID: TStringField;
uqGetTaskRowPositionITEM_NUM: TSmallintField;
uqGetTaskMaxItemNumMAX_ITEM_NUM: TIntegerField;
uqTaskItemsTASK_ITEM_ID: TIntegerField;
uqGetTaskRowPositionTASK_ITEM_ID: TIntegerField;
procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet);
private
......
......@@ -17,7 +17,7 @@ const
type
TTaskItem = class
public
taskItemId: string;
taskItemId: integer;
taskId: string;
itemNum: integer;
application: string;
......@@ -70,7 +70,7 @@ type
TTaskRowSave = class
public
taskItemId: string;
taskItemId: integer;
taskId: string;
taskNum: integer;
application: string;
......@@ -92,9 +92,10 @@ type
IApiService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
function GetTaskItems(taskId: string): TTaskItemsResponse;
// [HttpPost] function AddTaskRow(taskId: string): Boolean;
[HttpPost] function AddTaskRow(taskId: string): Boolean;
[HttpPost] function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
end;
implementation
......
......@@ -18,13 +18,13 @@ type
strict private
apiDB: TApiDatabase;
private
procedure EnsureBlankWebTaskRow(const taskId: string);
function BuildTaskNumber: string;
function BuildTaskTitle(const taskNumber, projectName, subject: string): string;
// function AddTaskRow(taskId: string): Boolean;
function AddTaskRow(taskId: string): Boolean;
function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
function GetWebClientVersion: string;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
......@@ -128,22 +128,12 @@ begin
apiDB.uqTaskItems.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskItems.Open;
if apiDB.uqTaskItems.IsEmpty then
begin
Logger.Log(4, Format('ApiService.GetTaskItems - no rows for TASK_ID="%s", ensuring blank row', [taskId]));
EnsureBlankWebTaskRow(taskId);
apiDB.uqTaskItems.Close;
apiDB.uqTaskItems.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskItems.Open;
end;
while not apiDB.uqTaskItems.Eof do
begin
item := TTaskItem.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(item);
item.taskItemId := apiDB.uqTaskItemsTASK_ITEM_ID.AsString;
item.taskItemId := apiDB.uqTaskItemsTASK_ITEM_ID.AsInteger;
item.taskId := apiDB.uqTaskItemsTASK_ID.AsString;
item.itemNum := apiDB.uqTaskItemsITEM_NUM.AsInteger;
......@@ -186,17 +176,118 @@ begin
end;
procedure TApiService.EnsureBlankWebTaskRow(const taskId: string);
function TApiService.AddTaskRow(taskId: string): Boolean;
begin
Logger.Log(4, Format('ApiService.EnsureBlankWebTaskRow - TASK_ID="%s"', [taskId]));
Logger.Log(4, Format('ApiService.AddTaskRow - TASK_ID="%s"', [taskId]));
try
apiDB.uqEnsureBlankRow.Close;
apiDB.uqEnsureBlankRow.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqEnsureBlankRow.ExecSQL;
apiDB.uqAddTaskRow.Close;
apiDB.uqAddTaskRow.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqAddTaskRow.ExecSQL;
Result := True;
Logger.Log(4, 'ApiService.AddTaskRow - OK');
except
on E: Exception do
begin
Logger.Log(2, 'ApiService.EnsureBlankWebTaskRow - ERROR: ' + E.Message);
Logger.Log(2, 'ApiService.AddTaskRow - ERROR: ' + E.Message);
raise;
end;
end;
end;
procedure TApiService.MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
var
oldItemNum: Integer;
maxItemNum: Integer;
finalItemNum: Integer;
tempItemNum: Integer;
begin
logger.log(4, 'MoveTaskRow start TASK_ID=' + IntToStr(taskId) +
' TASK_ITEM_ID=' + IntToStr(taskItemId) +
' NEW_ITEM_NUM=' + IntToStr(newItemNum));
apiDB.uqGetTaskRowPosition.Close;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqGetTaskRowPosition.ParamByName('TASK_ITEM_ID').AsInteger := taskItemId;
apiDB.uqGetTaskRowPosition.Open;
if apiDB.uqGetTaskRowPosition.IsEmpty then
begin
logger.log(2, 'MoveTaskRow row not found TASK_ID=' + IntToStr(taskId) +
' TASK_ITEM_ID=' + IntToStr(taskItemId));
raise Exception.Create('Task row not found.');
end;
oldItemNum := apiDB.uqGetTaskRowPositionITEM_NUM.AsInteger;
apiDB.uqGetTaskMaxItemNum.Close;
apiDB.uqGetTaskMaxItemNum.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqGetTaskMaxItemNum.Open;
maxItemNum := apiDB.uqGetTaskMaxItemNumMAX_ITEM_NUM.AsInteger;
finalItemNum := newItemNum;
if finalItemNum < 1 then
finalItemNum := 1;
if finalItemNum > maxItemNum then
finalItemNum := maxItemNum;
if finalItemNum = oldItemNum then
begin
logger.log(4, 'MoveTaskRow no move needed TASK_ID=' + IntToStr(taskId) +
' TASK_ITEM_ID=' + IntToStr(taskItemId) +
' ITEM_NUM=' + IntToStr(oldItemNum));
Exit;
end;
tempItemNum := -1;
apiDB.uqGetTaskRowPosition.Connection.StartTransaction;
try
apiDB.uqMoveTaskRowTemp.ParamByName('TEMP_ITEM_NUM').AsInteger := tempItemNum;
apiDB.uqMoveTaskRowTemp.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqMoveTaskRowTemp.ParamByName('TASK_ITEM_ID').AsInteger := taskItemId;
apiDB.uqMoveTaskRowTemp.ExecSQL;
if finalItemNum < oldItemNum then
begin
apiDB.uqShiftTaskRowsDown.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqShiftTaskRowsDown.ParamByName('NEW_ITEM_NUM').AsInteger := finalItemNum;
apiDB.uqShiftTaskRowsDown.ParamByName('OLD_ITEM_NUM').AsInteger := oldItemNum;
apiDB.uqShiftTaskRowsDown.ExecSQL;
end
else
begin
apiDB.uqShiftTaskRowsUp.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqShiftTaskRowsUp.ParamByName('OLD_ITEM_NUM').AsInteger := oldItemNum;
apiDB.uqShiftTaskRowsUp.ParamByName('NEW_ITEM_NUM').AsInteger := finalItemNum;
apiDB.uqShiftTaskRowsUp.ExecSQL;
end;
apiDB.uqSetTaskRowFinalPosition.ParamByName('TASK_ID').AsInteger := taskId;
apiDB.uqSetTaskRowFinalPosition.ParamByName('TASK_ITEM_ID').AsInteger := taskItemId;
apiDB.uqSetTaskRowFinalPosition.ParamByName('NEW_ITEM_NUM').AsInteger := finalItemNum;
apiDB.uqSetTaskRowFinalPosition.ExecSQL;
apiDB.uqGetTaskRowPosition.Connection.Commit;
logger.log(4, 'MoveTaskRow success TASK_ID=' + IntToStr(taskId) +
' TASK_ITEM_ID=' + IntToStr(taskItemId) +
' OLD_ITEM_NUM=' + IntToStr(oldItemNum) +
' NEW_ITEM_NUM=' + IntToStr(finalItemNum));
except
on E: Exception do
begin
if apiDB.uqGetTaskRowPosition.Connection.InTransaction then
apiDB.uqGetTaskRowPosition.Connection.Rollback;
logger.log(2, 'MoveTaskRow failed TASK_ID=' + IntToStr(taskId) +
' TASK_ITEM_ID=' + IntToStr(taskItemId) +
' ERROR=' + E.Message);
raise;
end;
end;
......@@ -258,12 +349,12 @@ function TApiService.SaveTaskRow(Item: TTaskRowSave): Boolean;
var
d: TDateTime;
begin
Logger.Log(4, Format('ApiService.SaveTaskRow - TASK_ITEM_ID="%s"', [Item.taskItemId]));
Logger.Log(4, Format('ApiService.SaveTaskRow - TASK_ITEM_ID="%d"', [Item.taskItemId]));
try
apiDB.uqSaveTaskRow.Close;
apiDB.uqSaveTaskRow.ParamByName('TASK_ITEM_ID').AsString := Item.taskItemId;
apiDB.uqSaveTaskRow.ParamByName('TASK_ITEM_ID').AsInteger := Item.taskItemId;
apiDB.uqSaveTaskRow.ParamByName('APPLICATION').AsString := Item.application;
apiDB.uqSaveTaskRow.ParamByName('APP_VERSION').AsString := Item.version;
......
<div id="div_wrapper">
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom shadow-sm">
<div class="container-fluid">
<div class="d-flex align-items-center gap-2">
<a id="lbl_app_title" class="navbar-brand fw-semibold" href="index.html">Koehler-Gibson Orders</a>
<span id="lbl_version" class="badge text-bg-light border text-muted fw-normal"></span>
</div>
<div class="collapse navbar-collapse show" id="pnl_navbar_nav_dropdown">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" id="lnk_navbar_dropdown_menu_link"
role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user fa-fw"></i>
<span id="lbl_username" class="fw-semibold">Username</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="lnk_navbar_dropdown_menu_link">
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_home" href="#">
<i class="fa fa-home fa-fw"></i><span>Home</span>
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_user_profile" href="#">
<i class="fa fa-user fa-fw"></i><span>User Profile</span>
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2 text-danger" id="lbl_logout" href="#">
<i class="fa fa-sign-out fa-fw"></i><span>Logout</span>
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Toast -->
<div id="pnl_toast_wrapper" class="position-fixed top-0 start-0 mt-5 ms-4"
style="z-index: 1080; min-width: 300px; max-width: 500px;">
<div id="toast_bootstrap" class="toast align-items-center text-white bg-success border-0 shadow" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="lbl_bootstrap_toast_body">
Success message
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
</div>
<!-- Main Panel (where all forms display) -->
<div class="container-fluid py-3">
<div class="row">
<div id="pnl_main" class="col-12"></div>
</div>
<div class="row mt-3">
<div class="col-12">
<textarea class="form-control font-monospace" id="memo_debug" rows="4" placeholder="Debug output..."></textarea>
</div>
</div>
</div>
<!-- Spinner Modal -->
<div id="div_spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller">
<div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div>
</div>
</div>
<!-- Error Modal -->
<div class="modal fade" id="mdl_error" tabindex="-1" aria-labelledby="lbl_modal_title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_modal_title">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_modal_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_restart" class="btn btn-primary">Back to Orders</button>
</div>
</div>
</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>
</div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div class="modal fade" id="mdl_notification" tabindex="-1" aria-labelledby="lbl_notification_title"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_notification_title">Info</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_notification_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_close" class="btn btn-primary">Close</button>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid p-2 d-flex flex-column h-100">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0">
<h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-outline-success">Add Row</button>
<button id="btn_reload" class="btn btn-sm btn-outline-primary">Reload</button>
</div>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNameManager" aria-labelledby="nm_title">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="nm_title">Add Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div id="nm_existing_list" class="list-group mb-3"></div>
<div id="nm_add_wrap" class="d-none mb-3">
<input id="nm_name_input" type="text" class="form-control" maxlength="100">
<div id="nm_name_invalid" class="invalid-feedback d-none"></div>
<div class="d-flex justify-content-end mt-2">
<button id="btn_nm_save" type="button" class="btn btn-success">Save</button>
</div>
</div>
<button id="btn_nm_add_another" type="button" class="btn btn-secondary">
Add another item
</button>
</div>
</div>
</div>
<div class="container py-4">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<div id="lbl_test" class="h2 fw-semibold mb-1">Test Form</div>
<div class="text-muted">Quick API + JWT/version diagnostics</div>
</div>
<button id="btn_test_api" class="btn btn-primary">Test API</button>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary d-flex align-items-center justify-content-between">
<div class="fw-semibold">Debug Output</div>
</div>
<div class="card-body" style="min-height: 70vh;">
<textarea id="memo_test_debug"
class="form-control font-monospace h-100"
style="min-height: 65vh;"
rows="28"
placeholder="Click Test API to populate diagnostics..."></textarea>
</div>
</div>
</div>
{
"AuthUrl" : "http://localhost:2001/emsys/emt3/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emt3/api/"
}
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
.is-invalid .form-check-label {
color: #dc3545 !important;
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
@keyframes slideInLeft {
from {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.slide-in {
animation: slideInLeft 0.4s ease-out forwards;
}
#spinner {
position: fixed !important;
z-index: 9999 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #204d74;
margin: -5px 0 0 -5px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="data:;base64,=" rel="icon"/>
<title>EM Systems Template App</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="emT3WebApp.js"></script>
</head>
<body>
<noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script>
</body>
</html>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{2A3028D9-BC39-4625-9BA5-0338012E2824}</ProjectGuid>
<ProjectVersion>20.3</ProjectVersion>
<ProjectVersion>20.4</ProjectVersion>
<FrameworkType>VCL</FrameworkType>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
......@@ -872,6 +872,9 @@
<Platform Name="Win64x">
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceDebug">
<Platform Name="iOSDevice32">
......@@ -942,6 +945,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo44">
<Platform Name="Win32">
......@@ -952,6 +959,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iOS_AppStore1024">
<Platform Name="iOSDevice64">
......@@ -1165,6 +1176,7 @@
<ProjectRoot Platform="Win32" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64x" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="WinARM64EC" Name="$(PROJECTNAME)"/>
</Deployment>
<Platforms>
<Platform value="Win32">True</Platform>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment