Commit 21727feb by Mac Stephens

Merge branch 'dev'

parents cdb35e6e 68ac3ea9
...@@ -4,11 +4,14 @@ emT3VCLDemo/Win64x/ ...@@ -4,11 +4,14 @@ emT3VCLDemo/Win64x/
emT3Web/__history/ emT3Web/__history/
emT3Web/__recovery/ emT3Web/__recovery/
emT3Web/config/ emT3Web/config/
emT3Web/css/__history/
emT3Web/Win32/ emT3Web/Win32/
emT3XDataServer/__history/ emT3XDataServer/__history/
emT3XDataServer/__recovery/
emT3XDataServer/bin/logs/ emT3XDataServer/bin/logs/
emT3XDataServer/bin/static/ emT3XDataServer/bin/static/
emT3XDataServer/Source/__history/ emT3XDataServer/Source/__history/
emT3XDataServer/Source/__recovery/
emT3XDataServer/Win32/ emT3XDataServer/Win32/
*.exe *.exe
...@@ -19,6 +22,3 @@ emT3XDataServer/Win32/ ...@@ -19,6 +22,3 @@ emT3XDataServer/Win32/
*.tvsconfig *.tvsconfig
*.txt *.txt
*.zip *.zip
emT3Web/css/__history/
...@@ -184,13 +184,13 @@ ...@@ -184,13 +184,13 @@
<BuildOrder>1</BuildOrder> <BuildOrder>1</BuildOrder>
<PCH>true</PCH> <PCH>true</PCH>
</PCHCompile> </PCHCompile>
<CppCompile Include="uMain.cpp"> <CppCompile Include="fMain.cpp">
<Form>fMain</Form> <Form>frmMain</Form>
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DependentOn>uMain.h</DependentOn> <DependentOn>fMain.h</DependentOn>
<BuildOrder>2</BuildOrder> <BuildOrder>2</BuildOrder>
</CppCompile> </CppCompile>
<FormResources Include="uMain.dfm"/> <FormResources Include="fMain.dfm"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
#pragma hdrstop #pragma hdrstop
#include <tchar.h> #include <tchar.h>
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
USEFORM("uMain.cpp", fMain); USEFORM("fMain.cpp", frmMain);
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{ {
...@@ -12,7 +12,7 @@ int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) ...@@ -12,7 +12,7 @@ int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{ {
Application->Initialize(); Application->Initialize();
Application->MainFormOnTaskBar = true; Application->MainFormOnTaskBar = true;
Application->CreateForm(__classid(TfMain), &fMain); Application->CreateForm(__classid(TfrmMain), &frmMain);
Application->Run(); Application->Run();
} }
catch (Exception &exception) catch (Exception &exception)
......
...@@ -5,33 +5,35 @@ ...@@ -5,33 +5,35 @@
#include <System.NetEncoding.hpp> #include <System.NetEncoding.hpp>
#pragma hdrstop #pragma hdrstop
#include "uMain.h" #include "fMain.h"
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
#pragma package(smart_init) #pragma package(smart_init)
#pragma link "MySQLUniProvider" #pragma link "MySQLUniProvider"
#pragma link "UniProvider" #pragma link "UniProvider"
#pragma resource "*.dfm" #pragma resource "*.dfm"
TfMain *fMain; TfrmMain *frmMain;
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
__fastcall TfMain::TfMain(TComponent* Owner) __fastcall TfrmMain::TfrmMain(TComponent* Owner)
: TForm(Owner) : TForm(Owner)
{ {
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
void __fastcall TfrmMain::FormCreate(TObject *Sender)
void __fastcall TfMain::FormCreate(TObject *Sender)
{ {
Randomize(); Randomize();
LogLine("App started"); LogLine( "ucETask->Server: " + ucETask->Server );
LogLine( "ucETask->Database: " + ucETask->Database );
LogLine( "ucETask->Username: " + ucETask->Username );
LogLine( "ucETask->Password: " + ucETask->Password );
LogLine( "App started" );
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
void __fastcall TfrmMain::btnOpenTaskItemsClick(TObject *Sender)
void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender)
{ {
const UnicodeString userId = edtUserId->Text.Trim(); String userId = edtUserId->Text.Trim();
const UnicodeString taskId = edtTaskId->Text.Trim(); String taskId = edtTaskId->Text.Trim();
const UnicodeString baseUrl = edtWebUrl->Text.Trim(); String baseUrl = edtWebUrl->Text.Trim();
const int expSeconds = StrToIntDef(edtExpSeconds->Text.Trim(), 60); int expSeconds = StrToIntDef(edtExpSeconds->Text.Trim(), 5);
if (userId.IsEmpty() || taskId.IsEmpty() || baseUrl.IsEmpty()) if (userId.IsEmpty() || taskId.IsEmpty() || baseUrl.IsEmpty())
{ {
...@@ -39,14 +41,14 @@ void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender) ...@@ -39,14 +41,14 @@ void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender)
return; return;
} }
const int codeInt = 100000 + Random(900000); int codeInt = 100000 + Random(900000);
const UnicodeString urlCode = IntToStr(codeInt); String urlCode = IntToStr(codeInt);
LogLine("Generated URL_CODE=" + urlCode); LogLine("Generated URL_CODE=" + urlCode);
try try
{ {
if (!ucETask->Connected) if( !ucETask->Connected )
{ {
LogLine("Connecting to MariaDB..."); LogLine("Connecting to MariaDB...");
ucETask->Connect(); ucETask->Connect();
...@@ -75,42 +77,97 @@ void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender) ...@@ -75,42 +77,97 @@ void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender)
return; return;
} }
const UnicodeString launchUrl = BuildLaunchUrl(baseUrl, userId, taskId, urlCode); String launchUrl = BuildLaunchUrl(baseUrl, userId, taskId, urlCode);
if (launchUrl.IsEmpty()) if( launchUrl.IsEmpty() ){
{
LogLine("Launch URL build failed"); LogLine("Launch URL build failed");
return; return;
} }
LogLine("Launching browser:"); LogLine( "Launching browser:" );
LogLine(launchUrl); LogLine( launchUrl );
ShellExecute(0, L"open", launchUrl.w_str(), 0, 0, SW_SHOWNORMAL); ShellExecute( 0, L"open", launchUrl.w_str(), 0, 0, SW_SHOWNORMAL );
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
void TfrmMain::LogLine(String message)
void TfMain::LogLine(const String &message)
{ {
const String stamp = FormatDateTime("yyyy-mm-dd hh:nn:ss.zzz", Now()); String stamp = FormatDateTime("yyyy-mm-dd hh:nn:ss.zzz", Now());
memoLog->Lines->Add(stamp + " " + message); memoLog->Lines->Add(stamp + " " + message);
memoLog->SelStart = memoLog->Text.Length(); memoLog->SelStart = memoLog->Text.Length();
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
String TfrmMain::BuildLaunchUrl(String baseUrl, String userId, String taskId, String code)
String TfMain::BuildLaunchUrl(const String &baseUrl, const String &userId, const String &taskId, const String &code)
{ {
String cleanBaseUrl = baseUrl.Trim(); String cleanBaseUrl = baseUrl.Trim();
if (cleanBaseUrl.IsEmpty()) if( cleanBaseUrl.IsEmpty() )
return ""; return "";
String sep = cleanBaseUrl.Pos("?") > 0 ? "&" : "?"; return cleanBaseUrl + "?user_id=" + userId + "&task_id=" + taskId + "&url_code=" + code;
}
//---------------------------------------------------------------------------
void __fastcall TfrmMain::btnTaskItemsLoginClick(TObject *Sender)
{
String userId = edtUserId->Text.Trim();
String taskId = edtTaskId->Text.Trim();
String baseUrl = edtWebUrl->Text.Trim();
if( userId.IsEmpty() || taskId.IsEmpty() || baseUrl.IsEmpty() )
{
LogLine( "Missing required input (userId, taskId, or webUrl)" );
return;
}
String qUserId = TNetEncoding::URL->Encode(userId); String urlCode = 'WebLogin';
String qTaskId = TNetEncoding::URL->Encode(taskId);
String qCode = TNetEncoding::URL->Encode(code);
return cleanBaseUrl + sep + "user_id=" + qUserId + "&task_id=" + qTaskId + "&url_code=" + qCode; LogLine( "Generated URL_CODE=" + urlCode );
}
try
{
if( !ucETask->Connected )
{
LogLine("Connecting to MariaDB...");
ucETask->Connect();
LogLine("Connected");
}
LogLine( "Inserting row into web_tasks_url..." );
uqWebTasksUrl->Close();
uqWebTasksUrl->SQL->Text =
"insert into web_tasks_url (USER_ID, TASK_ID, URL_CODE, URL_TIME, URL_TIME_EXP) "
"values (:USER_ID, :TASK_ID, :URL_CODE, NOW(), :URL_TIME_EXP)";
uqWebTasksUrl->ParamByName("USER_ID")->AsString = userId;
uqWebTasksUrl->ParamByName("TASK_ID")->AsString = taskId;
uqWebTasksUrl->ParamByName("URL_CODE")->AsString = urlCode;
uqWebTasksUrl->ParamByName("URL_TIME_EXP")->AsInteger = 0;
uqWebTasksUrl->ExecSQL();
LogLine("Insert OK");
}
catch (const Exception &e)
{
LogLine("DB ERROR: " + e.Message);
return;
}
String launchUrl = baseUrl + "?user_id=" + userId + "&task_id=" + taskId;
if( launchUrl.IsEmpty() ){
LogLine("Launch URL build failed");
return;
}
LogLine( "Task Items Login url:" + launchUrl );
//ShellExecute( 0, L"open", launchUrl.w_str(), 0, 0, SW_SHOWNORMAL );
}
//---------------------------------------------------------------------------
void __fastcall TfrmMain::btnConnectClick(TObject *Sender)
{
ucETask->Connect();
LogLine( "Connected to database" );
}
//---------------------------------------------------------------------------
object fMain: TfMain object frmMain: TfrmMain
Left = 0 Left = 0
Top = 0 Top = 0
Caption = 'fMain' Caption = 'frmMain'
ClientHeight = 441 ClientHeight = 524
ClientWidth = 624 ClientWidth = 810
Color = clBtnFace Color = clBtnFace
Font.Charset = DEFAULT_CHARSET Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText Font.Color = clWindowText
...@@ -52,12 +52,15 @@ object fMain: TfMain ...@@ -52,12 +52,15 @@ object fMain: TfMain
OnClick = btnOpenTaskItemsClick OnClick = btnOpenTaskItemsClick
end end
object memoLog: TMemo object memoLog: TMemo
Left = 20 Left = 0
Top = 102 Top = 108
Width = 581 Width = 810
Height = 311 Height = 416
Align = alBottom
Anchors = [akLeft, akTop, akRight, akBottom]
Lines.Strings = ( Lines.Strings = (
'') '')
ScrollBars = ssVertical
TabOrder = 1 TabOrder = 1
end end
object edtUserId: TEdit object edtUserId: TEdit
...@@ -89,6 +92,24 @@ object fMain: TfMain ...@@ -89,6 +92,24 @@ object fMain: TfMain
NumbersOnly = True NumbersOnly = True
TabOrder = 5 TabOrder = 5
end end
object btnTaskItemsLogin: TButton
Left = 140
Top = 64
Width = 109
Height = 25
Caption = 'Task Items Login'
TabOrder = 6
OnClick = btnTaskItemsLoginClick
end
object btnConnect: TButton
Left = 260
Top = 64
Width = 73
Height = 25
Caption = 'Connect'
TabOrder = 7
OnClick = btnConnectClick
end
object ucETask: TUniConnection object ucETask: TUniConnection
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'etask' Database = 'etask'
...@@ -105,7 +126,7 @@ object fMain: TfMain ...@@ -105,7 +126,7 @@ object fMain: TfMain
'MySQL.HttpTrustServerCertificate=False' 'MySQL.HttpTrustServerCertificate=False'
'MySQL.ProxyPort=0') 'MySQL.ProxyPort=0')
Username = 'root' Username = 'root'
Server = '192.168.102.131' Server = '192.168.116.131'
LoginPrompt = False LoginPrompt = False
Left = 390 Left = 390
Top = 342 Top = 342
......
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
#ifndef uMainH #ifndef fMainH
#define uMainH #define fMainH
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
#include <System.Classes.hpp> #include <System.Classes.hpp>
#include <Vcl.Controls.hpp> #include <Vcl.Controls.hpp>
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
#include "MySQLUniProvider.hpp" #include "MySQLUniProvider.hpp"
#include "UniProvider.hpp" #include "UniProvider.hpp"
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
class TfMain : public TForm class TfrmMain : public TForm
{ {
__published: // IDE-managed Components __published: // IDE-managed Components
TButton *btnOpenTaskItems; TButton *btnOpenTaskItems;
...@@ -30,16 +30,20 @@ __published: // IDE-managed Components ...@@ -30,16 +30,20 @@ __published: // IDE-managed Components
TUniConnection *ucETask; TUniConnection *ucETask;
TUniQuery *uqWebTasksUrl; TUniQuery *uqWebTasksUrl;
TMySQLUniProvider *MySQLUniProvider1; TMySQLUniProvider *MySQLUniProvider1;
TButton *btnTaskItemsLogin;
TButton *btnConnect;
void __fastcall FormCreate(TObject *Sender); void __fastcall FormCreate(TObject *Sender);
void __fastcall btnOpenTaskItemsClick(TObject *Sender); void __fastcall btnOpenTaskItemsClick(TObject *Sender);
void __fastcall btnTaskItemsLoginClick(TObject *Sender);
void __fastcall btnConnectClick(TObject *Sender);
private: private:
void LogLine(const String &message); void LogLine(String message);
String BuildLaunchUrl(const String &baseUrl, const String &userId, const String &taskId, const String &code); String BuildLaunchUrl(String baseUrl, String userId, String taskId, String code);
public: // User declarations public: // User declarations
__fastcall TfMain(TComponent* Owner); __fastcall TfrmMain(TComponent* Owner);
}; };
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
extern PACKAGE TfMain *fMain; extern PACKAGE TfrmMain *frmMain;
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
#endif #endif
...@@ -4,7 +4,7 @@ interface ...@@ -4,7 +4,7 @@ interface
uses uses
SysUtils, Web, JS, SysUtils, Web, JS,
XData.Web.Client; XData.Web.Client, WEBLib.Crypto;
const const
TOKEN_NAME = 'EMT3_WEB_TOKEN'; TOKEN_NAME = 'EMT3_WEB_TOKEN';
...@@ -23,8 +23,8 @@ type ...@@ -23,8 +23,8 @@ type
public public
constructor Create; reintroduce; constructor Create; reintroduce;
destructor Destroy; override; destructor Destroy; override;
procedure Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess; procedure Login(AUserId, ATaskId, AUrlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
AError: TOnLoginError); [async] procedure WebLogin(AUserName, ALoginType, ALoginValue, APassword: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure Logout; procedure Logout;
function GetToken: string; function GetToken: string;
function Authenticated: Boolean; function Authenticated: Boolean;
...@@ -91,7 +91,7 @@ begin ...@@ -91,7 +91,7 @@ begin
Result := window.localStorage.getItem(TOKEN_NAME); Result := window.localStorage.getItem(TOKEN_NAME);
end; end;
procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess; procedure TAuthService.Login(AUserId, ATaskId, AUrlCode: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError); AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse); procedure OnLoad(Response: TXDataClientResponse);
...@@ -109,21 +109,70 @@ procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: ...@@ -109,21 +109,70 @@ procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess:
end; end;
begin begin
if (AUser = '') or (APassword = '') then if (AUserId = '') or (ATaskId = '') or (AUrlCode = '') then
begin begin
AError('Invalid or expired code, please try again.'); AError('Invalid or expired code, please try again.');
Exit; Exit;
end; end;
FClient.RawInvoke( FClient.RawInvoke(
'IAuthService.Login', [AUser, APassword, AClientVersion], 'IAuthService.Login', [AUserId, ATaskId, AUrlCode],
@OnLoad, @OnError @OnLoad, @OnError
); );
end; end;
[async] procedure TAuthService.WebLogin(AUserName, ALoginType, ALoginValue, APassword: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
var
sha: TWebSHAHash;
passwordHash: string;
procedure OnLoad(Response: TXDataClientResponse);
var
Token: JS.TJSObject;
begin
Token := JS.TJSObject(Response.Result);
SetToken(JS.toString(Token.Properties['value']));
ASuccess;
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
if (AUserName = '') or (ALoginType = '') or (APassword = '') then
begin
AError('Please enter all required login fields.');
Exit;
end;
if SameText(ALoginType, 'task_items') and (ALoginValue = '') then
begin
AError('Please enter all required login fields.');
Exit;
end;
sha := TWebSHAHash.Create(ehSHA256);
try
passwordHash := Await(string, sha.Hash(UpperCase(Trim(APassword))));
finally
sha.Free;
end;
FClient.RawInvoke(
'IAuthService.WebLogin', [AUserName, ALoginType, ALoginValue, passwordHash],
@OnLoad, @OnError
);
end;
procedure TAuthService.Logout; procedure TAuthService.Logout;
begin begin
DeleteToken; DeleteToken;
DMConnection.currentTaskId := '';
DMConnection.currentTimeEntryDate := '';
end; end;
procedure TAuthService.SetToken(AToken: string); procedure TAuthService.SetToken(AToken: string);
......
...@@ -20,7 +20,9 @@ type ...@@ -20,7 +20,9 @@ type
FUnauthorizedAccessProc: TUnauthorizedAccessProc; FUnauthorizedAccessProc: TUnauthorizedAccessProc;
public public
const clientVersion = '0.8.8'; currentTaskId: string;
currentTimeEntryDate: string;
const clientVersion = '0.9.3';
procedure InitApp(SuccessProc: TSuccessProc; procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc); UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure SetClientConfig(Callback: TVersionCheckCallback); procedure SetClientConfig(Callback: TVersionCheckCallback);
......
...@@ -160,6 +160,12 @@ begin ...@@ -160,6 +160,12 @@ begin
var btnRight = document.getElementById('btn_confirm_right'); var btnRight = document.getElementById('btn_confirm_right');
var bsModal; 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 (body) body.innerText = msg;
if (btnLeft) btnLeft.innerText = leftLabel; if (btnLeft) btnLeft.innerText = leftLabel;
if (btnRight) btnRight.innerText = rightLabel; if (btnRight) btnRight.innerText = rightLabel;
......
object FViewErrorPage: TFViewErrorPage
Left = 0
Top = 0
Width = 534
Height = 426
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
TabOrder = 1
object lbTitle: TWebLabel
Left = 24
Top = 24
Width = 128
Height = 13
Caption = 'Oops... an error occurred!'
ElementID = 'view.errorpage.title'
Transparent = False
end
object lbMessage: TWebLabel
Left = 24
Top = 56
Width = 42
Height = 13
Caption = 'Message'
ElementID = 'view.errorpage.message'
Transparent = False
end
end
<div class="container">
<br />
<div class="panel panel-red">
<div id="view.errorpage.title" class="panel-heading">
Error Page
</div>
<div id="view.errorpage.message" class="panel-body">
Message
</div>
<div class="panel-footer">
<a href=".">Reload web application</a>
</div>
</div>
</div>
unit View.ErrorPage;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls,
XData.Web.Connection, WEBLib.StdCtrls;
type
TFViewErrorPage = class(TWebForm)
lbTitle: TWebLabel;
lbMessage: TWebLabel;
public
class procedure Display(AErrorMessage: string);
class procedure DisplayConnectionError(AError: TXDataWebConnectionError);
end;
var
FViewErrorPage: TFViewErrorPage;
implementation
{$R *.dfm}
{ TFViewErrorPage }
class procedure TFViewErrorPage.Display(AErrorMessage: string);
procedure AfterCreateProc(AForm: TObject);
begin
TFViewErrorPage(AForm).lbMessage.Caption := AErrorMessage;
end;
begin
if Assigned(FViewErrorPage) then
FViewErrorPage.Free;
FViewErrorPage := TFViewErrorPage.CreateNew(@AfterCreateProc);
end;
class procedure TFViewErrorPage.DisplayConnectionError(
AError: TXDataWebConnectionError);
begin
Display(AError.ErrorMessage + ': ' + AError.RequestUrl);
end;
end.
object FViewLogin: TFViewLogin
Width = 640
Height = 480
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
ParentFont = False
OnCreate = WebFormCreate
object WebLabel1: TWebLabel
Left = 240
Top = 112
Width = 67
Height = 13
Caption = 'Please Sign In'
ElementID = 'view.login.title'
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object edtUsername: TWebEdit
Left = 240
Top = 180
Width = 121
Height = 21
ElementID = 'view.login.edtusername'
HeightPercent = 100.000000000000000000
TextHint = 'Username'
WidthPercent = 100.000000000000000000
end
object edtPassword: TWebEdit
Left = 240
Top = 207
Width = 121
Height = 21
ElementID = 'view.login.edtpassword'
HeightPercent = 100.000000000000000000
PasswordChar = '*'
TabOrder = 1
TextHint = 'Password'
WidthPercent = 100.000000000000000000
end
object btnLogin: TWebButton
Left = 240
Top = 234
Width = 121
Height = 25
Caption = 'Login'
ElementID = 'view.login.btnlogin'
HeightPercent = 100.000000000000000000
TabOrder = 2
WidthPercent = 100.000000000000000000
OnClick = btnLoginClick
end
object pnlMessage: TWebPanel
Left = 240
Top = 65
Width = 121
Height = 33
ElementID = 'view.login.message'
TabOrder = 3
object lblMessage: TWebLabel
Left = 16
Top = 11
Width = 42
Height = 13
Caption = 'Message'
ElementID = 'view.login.message.label'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnCloseNotification: TWebButton
Left = 96
Top = 3
Width = 22
Height = 25
ElementID = 'view.login.message.button'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnCloseNotificationClick
end
end
object edtTaskId: TWebEdit
Left = 240
Top = 133
Width = 121
Height = 21
ChildOrder = 5
ElementID = 'view.login.edttaskid'
HeightPercent = 100.000000000000000000
TextHint = 'TaskId'
WidthPercent = 100.000000000000000000
end
object edtDate: TWebEdit
Left = 240
Top = 156
Width = 121
Height = 22
ChildOrder = 6
ElementID = 'view.login.edtdate'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
TextHint = 'Date'
WidthPercent = 100.000000000000000000
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.AuthConnection
Left = 492
Top = 102
end
end
<nav class="navbar navbar-light bg-light login-navbar">
<div class="container-fluid">
<a class="navbar-brand">emT3web</a>
</div>
</nav>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card login-card">
<div class="card-header">
<h3 id="view.login.title" class="fs-6 card-title">Please Sign In</h3>
</div>
<div class="card-body">
<div role="form">
<div id="view.login.message" class="alert alert-danger">
<button id="view.login.message.button" type="button" class="btn-close" aria-label="Close"></button>
<span id="view.login.message.label"></span>
</div>
<fieldset>
<div id="view.login.taskid.group" class="mb-3">
<input id="view.login.edttaskid" class="form-control" type="text" placeholder="Task Id">
</div>
<div id="view.login.date.group" class="mb-3">
<input id="view.login.edtdate" class="form-control" type="date" placeholder="Date">
</div>
<div class="mb-3">
<input id="view.login.edtusername" class="form-control" type="text" autofocus placeholder="Username">
</div>
<div class="mb-3">
<input id="view.login.edtpassword" class="form-control" type="password" placeholder="Password">
</div>
<div class="mb-3">
<button id="view.login.btnlogin" class="btn btn-primary w-100">Login</button>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
</div>
unit View.Login;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
JS, XData.Web.Connection, WEBLib.ExtCtrls, Web,
App.Types, ConnectionModule, XData.Web.Client;
type
TFViewLogin = class(TWebForm)
WebLabel1: TWebLabel;
edtUsername: TWebEdit;
edtPassword: TWebEdit;
btnLogin: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
btnCloseNotification: TWebButton;
XDataWebClient: TXDataWebClient;
edtTaskId: TWebEdit;
edtDate: TWebEdit;
procedure btnLoginClick(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
private
FLoginProc: TSuccessProc;
FMessage: string;
FUserId: string;
FTaskId: string;
procedure ShowNotification(Notification: string);
procedure HideNotification;
public
class procedure Display(LoginProc: TSuccessProc); overload;
class procedure Display(LoginProc: TSuccessProc; AMsg: string); overload;
class procedure Display(LoginProc: TSuccessProc; AUserId, ATaskId, AMsg: string); overload;
end;
var
FViewLogin: TFViewLogin;
implementation
uses
Auth.Service,
View.ErrorPage;
{$R *.dfm}
procedure TFViewLogin.btnLoginClick(Sender: TObject);
var
timeEntriesParam: string;
newUrl: string;
isTimeEntriesMode: Boolean;
procedure LoginSuccess;
begin
if isTimeEntriesMode then
begin
DMConnection.currentTaskId := '';
DMConnection.currentTimeEntryDate := edtDate.Text;
if edtDate.Text <> '' then
newUrl := window.location.pathname + '?time_entries=true&date=' + edtDate.Text
else
newUrl := window.location.pathname + '?time_entries=true';
window.history.replaceState(TJSObject.new, '', newUrl);
end
else
DMConnection.currentTaskId := edtTaskId.Text;
FLoginProc;
end;
procedure LoginError(AMsg: string);
begin
ShowNotification('Login Error: ' + AMsg);
end;
begin
timeEntriesParam := Application.Parameters.Values['time_entries'];
isTimeEntriesMode :=
SameText(timeEntriesParam, 'true') or
(Application.Parameters.Values['task_id'] = '');
if isTimeEntriesMode then
begin
AuthService.WebLogin(
edtUsername.Text,
'time_entries',
edtDate.Text,
edtPassword.Text,
@LoginSuccess,
@LoginError
);
end
else
begin
AuthService.WebLogin(
edtUsername.Text,
'task_items',
edtTaskId.Text,
edtPassword.Text,
@LoginSuccess,
@LoginError
);
end;
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc);
begin
TFViewLogin.Display(LoginProc, '', '', '');
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AMsg: string);
begin
TFViewLogin.Display(LoginProc, '', '', AMsg);
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AUserId, ATaskId, AMsg: string);
procedure FormCreate(AForm: TObject);
begin
TFViewLogin(AForm).FUserId := AUserId;
TFViewLogin(AForm).FTaskId := ATaskId;
TFViewLogin(AForm).FMessage := AMsg;
end;
begin
console.log('TFViewLogin.Display start AUserId=' + AUserId + ' ATaskId=' + ATaskId + ' AMsg=' + AMsg);
if Assigned(FViewLogin) then
FViewLogin.Free;
FViewLogin := TFViewLogin.CreateNew(@FormCreate);
FViewLogin.FLoginProc := LoginProc;
console.log('TFViewLogin.Display end');
end;
procedure TFViewLogin.HideNotification;
begin
pnlMessage.ElementHandle.hidden := True;
end;
procedure TFViewLogin.ShowNotification(Notification: string);
begin
if Notification <> '' then
begin
lblMessage.Caption := Notification;
pnlMessage.ElementHandle.hidden := False;
end;
end;
procedure TFViewLogin.WebFormCreate(Sender: TObject);
var
timeEntriesParam: string;
isTimeEntriesMode: Boolean;
begin
console.log('TFViewLogin.WebFormCreate FUserId=' + FUserId + ' FTaskId=' + FTaskId + ' FMessage=' + FMessage);
timeEntriesParam := Application.Parameters.Values['time_entries'];
isTimeEntriesMode :=
SameText(timeEntriesParam, 'true') or
(Application.Parameters.Values['task_id'] = '');
edtUsername.Text := FUserId;
edtTaskId.Text := FTaskId;
edtDate.Text := Application.Parameters.Values['date'];
if isTimeEntriesMode then
begin
WebLabel1.Caption := 'Time Entries Sign In';
TJSHTMLElement(document.getElementById('view.login.taskid.group')).classList.add('d-none');
TJSHTMLElement(document.getElementById('view.login.date.group')).classList.remove('d-none');
end
else
begin
WebLabel1.Caption := 'Task Items Sign In';
TJSHTMLElement(document.getElementById('view.login.taskid.group')).classList.remove('d-none');
TJSHTMLElement(document.getElementById('view.login.date.group')).classList.add('d-none');
end;
if FMessage <> '' then
ShowNotification(FMessage)
else
HideNotification;
end;
procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
end.
...@@ -17,18 +17,18 @@ ...@@ -17,18 +17,18 @@
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="lnk_navbar_dropdown_menu_link"> <ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="lnk_navbar_dropdown_menu_link">
<li> <li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_home" href="#"> <a class="dropdown-item d-flex align-items-center gap-2" id="lbl_home">
<i class="fa fa-home fa-fw"></i><span>Home</span> <i class="fa fa-home fa-fw"></i><span>Home</span>
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_user_profile" href="#"> <a class="dropdown-item d-flex align-items-center gap-2" id="lbl_user_profile">
<i class="fa fa-user fa-fw"></i><span>User Profile</span> <i class="fa fa-user fa-fw"></i><span>User Profile</span>
</a> </a>
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li> <li>
<a class="dropdown-item d-flex align-items-center gap-2 text-danger" id="lbl_logout" href="#"> <a class="dropdown-item d-flex align-items-center gap-2 text-danger" id="lbl_logout">
<i class="fa fa-sign-out fa-fw"></i><span>Logout</span> <i class="fa fa-sign-out fa-fw"></i><span>Logout</span>
</a> </a>
</li> </li>
...@@ -87,19 +87,23 @@ ...@@ -87,19 +87,23 @@
</div> </div>
<!-- Confirmation Modal --> <!-- Confirmation Modal -->
<div class="modal fade" id="mdl_confirmation" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content shadow-lg"> <div class="modal-content shadow-lg">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Confirm</h5> <h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body fw-bold" id="lbl_confirmation_body"> <div class="modal-body fw-bold" id="main_modal_body">
Placeholder text Placeholder text
</div> </div>
<div class="modal-footer justify-content-center"> <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-danger me-3" id="btn_confirm_left">
<button type="button" class="btn btn-secondary" id="btn_confirm_right">Confirm</button> Delete
</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">
Cancel
</button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -35,19 +35,34 @@ implementation ...@@ -35,19 +35,34 @@ implementation
uses uses
Auth.Service, Auth.Service,
View.TaskItems; View.TaskItems,
View.TimeEntries;
{$R *.dfm} {$R *.dfm}
procedure TFViewMain.WebFormCreate(Sender: TObject); procedure TFViewMain.WebFormCreate(Sender: TObject);
var var
userName: string; userName: string;
isTimeEntriesMode: Boolean;
begin begin
userName := JS.toString(AuthService.TokenPayload.Properties['user_name']); userName := JS.toString(AuthService.TokenPayload.Properties['user_name']);
lblUsername.Caption := userName; lblUsername.Caption := userName;
lblVersion.Caption := 'v' + DMConnection.clientVersion; lblVersion.Caption := 'v' + DMConnection.clientVersion;
isTimeEntriesMode :=
SameText(Application.Parameters.Values['time_entries'], 'true') or
(Application.Parameters.Values['task_id'] = '');
if isTimeEntriesMode then
begin
lblAppTitle.Caption := 'Time Entries';
ShowForm(TFTimeEntries);
end
else
begin
lblAppTitle.Caption := 'Task Items';
ShowForm(TFTaskItems); ShowForm(TFTaskItems);
end;
end; end;
procedure TFViewMain.lblLogoutClick(Sender: TObject); procedure TFViewMain.lblLogoutClick(Sender: TObject);
......
...@@ -58,6 +58,62 @@ object FTaskItems: TFTaskItems ...@@ -58,6 +58,62 @@ object FTaskItems: TFTaskItems
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnInsertRowClick OnClick = btnInsertRowClick
end end
object btnTextColor: TWebButton
Left = 282
Top = 197
Width = 96
Height = 25
Caption = 'Text Color'
ChildOrder = 4
ElementID = 'btn_text_color'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnTextColorClick
end
object btnFmtBold: TWebButton
Left = 282
Top = 228
Width = 96
Height = 25
Caption = 'Bold'
ChildOrder = 4
ElementID = 'btn_fmt_bold'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtBoldClick
end
object btnFmtItalic: TWebButton
Left = 282
Top = 259
Width = 96
Height = 25
Caption = 'Italic'
ChildOrder = 4
ElementID = 'btn_fmt_italic'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtItalicClick
end
object btnFmtUnderline: TWebButton
Left = 282
Top = 290
Width = 96
Height = 25
Caption = 'Underline'
ChildOrder = 4
ElementID = 'btn_fmt_underline'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFmtUnderlineClick
end
object xdwcTasks: TXDataWebClient object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 506 Left = 506
......
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden"> <div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0"> <div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0 gap-3">
<h5 class="mb-0" id="lbl_project_name"></h5> <h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
<div id="lbl_total_rows"></div> <div id="lbl_total_rows" class="me-2 text-nowrap"></div>
<div class="d-flex gap-2"> <div class="task-toolbar d-flex align-items-center gap-1 px-2 py-1">
<button id="btn_add_row" class="btn btn-sm btn-success">Add Row</button> <button id="btn_fmt_bold" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Bold">
<button id="btn_delete_row" class="btn btn-sm btn-danger">Delete Row</button> <i class="fas fa-bold"></i>
<button id="btn_insert_row" class="btn btn-sm btn-success">Insert Row</button> </button>
<button id="btn_reload" class="btn btn-sm btn-primary">Reload</button> <button id="btn_fmt_italic" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Italic">
<i class="fas fa-italic"></i>
</button>
<button id="btn_fmt_underline" type="button" class="btn btn-sm btn-light task-toolbar-btn" title="Underline">
<i class="fas fa-underline"></i>
</button>
<div class="vr mx-1"></div>
<button id="btn_text_color" type="button" class="btn btn-sm btn-light task-toolbar-btn task-color-btn" title="Text color">
<span class="task-color-glyph">A</span>
<span id="text_color_swatch" class="task-color-swatch"></span>
<input id="issue_notes_text_color" type="color" class="task-hidden-color-input" value="#000000">
</button>
</div> </div>
<button id="btn_add_row" class="btn btn-sm btn-success text-nowrap">Add Row</button>
<button id="btn_insert_row" class="btn btn-sm btn-success text-nowrap">Insert Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-danger text-nowrap">Delete Row</button>
<button id="btn_reload" class="btn btn-sm btn-primary text-nowrap">Reload</button>
</div> </div>
</div> </div>
......
object FTimeEntries: TFTimeEntries
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object lblValidationMessage: TWebLabel
Left = 90
Top = 32
Width = 3
Height = 15
ElementID = 'lbl_validation_message'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object edtWeekOf: TWebEdit
Left = 71
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 1
ElementID = 'edt_week_of'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtWeekOf'
WidthPercent = 100.000000000000000000
OnExit = edtWeekOfExit
end
object edtStartDate: TWebEdit
Left = 245
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 2
ElementID = 'edt_start_date'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtStartDate'
WidthPercent = 100.000000000000000000
end
object edtEndDate: TWebEdit
Left = 415
Top = 80
Width = 121
Height = 22
TabStop = False
ChildOrder = 3
ElementID = 'edt_end_date'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
ShowFocus = False
Text = 'edtEndDate'
WidthPercent = 100.000000000000000000
end
object btnAddEntry: TWebButton
Left = 440
Top = 24
Width = 96
Height = 25
Caption = 'Add Entry'
ChildOrder = 3
ElementID = 'btn_add_entry'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnAddEntryClick
end
object 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 btnCopyEntry: TWebButton
Left = 440
Top = 122
Width = 96
Height = 25
Caption = 'Copy'
ChildOrder = 7
ElementID = 'btn_copy_entry'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnCopyEntryClick
end
object xdwcTimeEntries: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 460
Top = 156
end
object xdwdsTimeEntries: TXDataWebDataSet
Left = 322
Top = 156
object xdwdsTimeEntriesentryId: TIntegerField
FieldName = 'entryId'
end
object xdwdsTimeEntriestaskDate: TStringField
FieldName = 'taskDate'
end
object xdwdsTimeEntriestaskId: TStringField
FieldName = 'taskId'
end
object xdwdsTimeEntriestaskDisplay: TStringField
FieldName = 'taskDisplay'
end
object xdwdsTimeEntrieshours: TFloatField
FieldName = 'hours'
end
object xdwdsTimeEntriestaskTime: TStringField
FieldName = 'taskTime'
end
object xdwdsTimeEntriescategory: TStringField
FieldName = 'category'
end
object xdwdsTimeEntriescategoryDesc: TStringField
FieldName = 'categoryDesc'
end
object xdwdsTimeEntriessummary: TStringField
FieldName = 'summary'
end
object xdwdsTimeEntriesplace: TStringField
FieldName = 'place'
end
object xdwdsTimeEntriesplaceDesc: TStringField
FieldName = 'placeDesc'
end
end
end
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0 gap-3">
<h5 class="mb-0 text-nowrap" id="lbl_time_entries_title"></h5>
<div class="d-flex align-items-center gap-2 ms-auto flex-nowrap">
<div id="lbl_time_total_rows" class="me-2 text-nowrap"></div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_week_of" class="form-label mb-0 text-nowrap small">Week of</label>
<input id="edt_week_of" type="date" class="form-control form-control-sm time-date-picker" />
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_start_date" class="form-label mb-0 text-nowrap small">Start</label>
<input id="edt_start_date" type="date" class="form-control form-control-sm time-date-picker" />
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<label for="edt_end_date" class="form-label mb-0 text-nowrap small">End</label>
<input id="edt_end_date" type="date" class="form-control form-control-sm time-date-picker" />
</div>
<button id="btn_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_copy_entry" class="btn btn-sm btn-primary text-nowrap" disabled>
Copy 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 invisible small" style="min-height: 31px;"></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>
is-invalid .form-check-input { .is-invalid .form-check-input {
border: 1px solid #dc3545 !important; border: 1px solid #dc3545 !important;
} }
...@@ -22,6 +22,7 @@ is-invalid .form-check-input { ...@@ -22,6 +22,7 @@ is-invalid .form-check-input {
transform: translateX(-120%); transform: translateX(-120%);
opacity: 0; opacity: 0;
} }
to { to {
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
...@@ -39,37 +40,6 @@ is-invalid .form-check-input { ...@@ -39,37 +40,6 @@ is-invalid .form-check-input {
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.tasks-vscroll {
height: 100%;
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
span.card { span.card {
border: none; border: none;
...@@ -89,155 +59,6 @@ span.card { ...@@ -89,155 +59,6 @@ span.card {
user-select: none; user-select: none;
} }
.tasks-table {
table-layout: fixed;
}
.tasks-table th {
overflow: hidden;
}
.tasks-table td {
overflow: visible;
}
.nowrap-cell,
.wrap-cell {
overflow: visible;
}
.tasks-table .dropdown,
.task-dd-toggle,
.task-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.dropdown-menu { .dropdown-menu {
z-index: 1055; z-index: 1055;
} }
.task-dd-toggle.status-cannot-duplicate {
--bs-btn-color: #41464b;
--bs-btn-bg: #e2e3e5;
--bs-btn-border-color: #c4c8cb;
--bs-btn-hover-color: #41464b;
--bs-btn-hover-bg: #d3d4d5;
--bs-btn-hover-border-color: #b9bcc0;
--bs-btn-active-color: #41464b;
--bs-btn-active-bg: #c6c7c8;
--bs-btn-active-border-color: #b9bcc0;
}
.task-dd-toggle.status-cannot-test {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-future-enhancement {
--bs-btn-color: #055160;
--bs-btn-bg: #cff4fc;
--bs-btn-border-color: #9eeaf9;
--bs-btn-hover-color: #055160;
--bs-btn-hover-bg: #b6effb;
--bs-btn-hover-border-color: #86e5f8;
--bs-btn-active-color: #055160;
--bs-btn-active-bg: #9eeaf9;
--bs-btn-active-border-color: #74dff6;
}
.task-dd-toggle.status-fixed-verified {
--bs-btn-color: #0f5132;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #0f5132;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #0f5132;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-fixed {
--bs-btn-color: #146c43;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #146c43;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #146c43;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-investigating {
--bs-btn-color: #664d03;
--bs-btn-bg: #fff3cd;
--bs-btn-border-color: #ffe69c;
--bs-btn-hover-color: #664d03;
--bs-btn-hover-bg: #ffecb5;
--bs-btn-hover-border-color: #ffdf7e;
--bs-btn-active-color: #664d03;
--bs-btn-active-bg: #ffe69c;
--bs-btn-active-border-color: #ffd966;
}
.task-dd-toggle.status-not-fixed {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-non-issue {
--bs-btn-color: #432874;
--bs-btn-bg: #e2d9f3;
--bs-btn-border-color: #cbbbe8;
--bs-btn-hover-color: #432874;
--bs-btn-hover-bg: #d6caee;
--bs-btn-hover-border-color: #bea9e2;
--bs-btn-active-color: #432874;
--bs-btn-active-bg: #cbbbe8;
--bs-btn-active-border-color: #b89ddd;
}
.task-dd-toggle.status-possibly-a-problem {
--bs-btn-color: #7a3e00;
--bs-btn-bg: #ffe5d0;
--bs-btn-border-color: #f7c79d;
--bs-btn-hover-color: #7a3e00;
--bs-btn-hover-bg: #ffd7b8;
--bs-btn-hover-border-color: #f2ba88;
--bs-btn-active-color: #7a3e00;
--bs-btn-active-bg: #f7c79d;
--bs-btn-active-border-color: #eeaf69;
}
.task-dd-toggle.status-default {
--bs-btn-color: #212529;
--bs-btn-bg: #f8f9fa;
--bs-btn-border-color: #dee2e6;
--bs-btn-hover-color: #212529;
--bs-btn-hover-bg: #e9ecef;
--bs-btn-hover-border-color: #ced4da;
--bs-btn-active-color: #212529;
--bs-btn-active-bg: #dee2e6;
--bs-btn-active-border-color: #ced4da;
}
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
.is-invalid .form-check-label {
color: #dc3545 !important;
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
@keyframes slideInLeft {
from {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.slide-in {
animation: slideInLeft 0.4s ease-out forwards;
}
#spinner {
position: fixed !important;
z-index: 9999 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.tasks-vscroll {
height: 100%;
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
span.card {
border: none;
}
.th-resize {
position: relative;
}
.th-resize-handle {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.tasks-table {
table-layout: fixed;
}
.tasks-table th {
overflow: hidden;
}
.tasks-table td {
overflow: visible;
}
.nowrap-cell,
.wrap-cell {
overflow: visible;
}
.tasks-table .dropdown,
.task-dd-toggle,
.task-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.dropdown-menu {
z-index: 1055;
}
.task-toolbar {
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
background: var(--bs-body-bg);
white-space: nowrap;
}
.task-toolbar-btn {
min-width: 2rem;
padding: 0.25rem 0.5rem;
}
#lbl_total_rows {
white-space: nowrap;
}
.task-rich-editor {
white-space: pre-wrap;
overflow-wrap: anywhere;
height: auto;
}
.task-rich-editor:focus {
outline: 0;
}
.task-color-btn {
position: relative;
min-width: 2rem;
height: 2rem;
padding: 0.2rem 0.45rem;
}
.task-hidden-color-input {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
width: 1px;
height: 1px;
border: 0;
}
.task-color-glyph {
display: inline-block;
font-weight: 600;
line-height: 1;
}
.task-color-swatch {
position: absolute;
left: 0.35rem;
right: 0.35rem;
bottom: 0.2rem;
height: 0.18rem;
border-radius: 999px;
background: #000000;
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th,
.time-table td {
overflow: hidden;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.task-dd-toggle.status-cannot-duplicate {
--bs-btn-color: #41464b;
--bs-btn-bg: #e2e3e5;
--bs-btn-border-color: #c4c8cb;
--bs-btn-hover-color: #41464b;
--bs-btn-hover-bg: #d3d4d5;
--bs-btn-hover-border-color: #b9bcc0;
--bs-btn-active-color: #41464b;
--bs-btn-active-bg: #c6c7c8;
--bs-btn-active-border-color: #b9bcc0;
}
.task-dd-toggle.status-cannot-test {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-future-enhancement {
--bs-btn-color: #055160;
--bs-btn-bg: #cff4fc;
--bs-btn-border-color: #9eeaf9;
--bs-btn-hover-color: #055160;
--bs-btn-hover-bg: #b6effb;
--bs-btn-hover-border-color: #86e5f8;
--bs-btn-active-color: #055160;
--bs-btn-active-bg: #9eeaf9;
--bs-btn-active-border-color: #74dff6;
}
.task-dd-toggle.status-fixed-verified {
--bs-btn-color: #0f5132;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #0f5132;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #0f5132;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-fixed {
--bs-btn-color: #146c43;
--bs-btn-bg: #d1e7dd;
--bs-btn-border-color: #a3cfbb;
--bs-btn-hover-color: #146c43;
--bs-btn-hover-bg: #badbcc;
--bs-btn-hover-border-color: #8fc5a9;
--bs-btn-active-color: #146c43;
--bs-btn-active-bg: #a3cfbb;
--bs-btn-active-border-color: #7db89d;
}
.task-dd-toggle.status-investigating {
--bs-btn-color: #664d03;
--bs-btn-bg: #fff3cd;
--bs-btn-border-color: #ffe69c;
--bs-btn-hover-color: #664d03;
--bs-btn-hover-bg: #ffecb5;
--bs-btn-hover-border-color: #ffdf7e;
--bs-btn-active-color: #664d03;
--bs-btn-active-bg: #ffe69c;
--bs-btn-active-border-color: #ffd966;
}
.task-dd-toggle.status-not-fixed {
--bs-btn-color: #842029;
--bs-btn-bg: #f8d7da;
--bs-btn-border-color: #f1aeb5;
--bs-btn-hover-color: #842029;
--bs-btn-hover-bg: #f1c2c7;
--bs-btn-hover-border-color: #ea9ca6;
--bs-btn-active-color: #842029;
--bs-btn-active-bg: #eaadb5;
--bs-btn-active-border-color: #e68592;
}
.task-dd-toggle.status-non-issue {
--bs-btn-color: #432874;
--bs-btn-bg: #e2d9f3;
--bs-btn-border-color: #cbbbe8;
--bs-btn-hover-color: #432874;
--bs-btn-hover-bg: #d6caee;
--bs-btn-hover-border-color: #bea9e2;
--bs-btn-active-color: #432874;
--bs-btn-active-bg: #cbbbe8;
--bs-btn-active-border-color: #b89ddd;
}
.task-dd-toggle.status-possibly-a-problem {
--bs-btn-color: #7a3e00;
--bs-btn-bg: #ffe5d0;
--bs-btn-border-color: #f7c79d;
--bs-btn-hover-color: #7a3e00;
--bs-btn-hover-bg: #ffd7b8;
--bs-btn-hover-border-color: #f2ba88;
--bs-btn-active-color: #7a3e00;
--bs-btn-active-bg: #f7c79d;
--bs-btn-active-border-color: #eeaf69;
}
.task-dd-toggle.status-default {
--bs-btn-color: #212529;
--bs-btn-bg: #f8f9fa;
--bs-btn-border-color: #dee2e6;
--bs-btn-hover-color: #212529;
--bs-btn-hover-bg: #e9ecef;
--bs-btn-hover-border-color: #ced4da;
--bs-btn-active-color: #212529;
--bs-btn-active-bg: #dee2e6;
--bs-btn-active-border-color: #ced4da;
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th {
overflow: hidden;
}
.time-table td {
overflow: visible;
}
.time-table .nowrap-cell,
.time-table .wrap-cell {
overflow: visible;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.time-table .dropdown-menu {
z-index: 1055;
}
.time-dd-toggle.is-invalid {
border-color: var(--bs-danger) !important;
padding-right: calc(1.5em + .75rem);
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ program emT3Web; ...@@ -2,6 +2,7 @@ program emT3Web;
uses uses
System.Classes, System.Classes,
System.SysUtils,
Vcl.Forms, Vcl.Forms,
XData.Web.Connection, XData.Web.Connection,
WEBLib.Dialogs, WEBLib.Dialogs,
...@@ -13,10 +14,15 @@ uses ...@@ -13,10 +14,15 @@ uses
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.TaskItems in 'View.TaskItems.pas' {FTaskItems: TWebForm} {*.html}, View.TaskItems in 'View.TaskItems.pas' {FTaskItems: TWebForm} {*.html},
uNameOffCanvas in 'uNameOffCanvas.pas', uNameOffCanvas in 'uNameOffCanvas.pas',
uDropdownHelpers in 'uDropdownHelpers.pas'; 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},
uTaskPickerOffCanvas in 'uTaskPickerOffCanvas.pas';
{$R *.res} {$R *.res}
procedure DisplayLoginView(AUserId: string = ''; ATaskId: string = ''; AMessage: string = ''); forward;
procedure DoLogout(AMsg: string = ''); forward; procedure DoLogout(AMsg: string = ''); forward;
procedure DisplayMainView; procedure DisplayMainView;
...@@ -42,48 +48,108 @@ procedure Login(userId: string; taskId: string; urlCode: string); ...@@ -42,48 +48,108 @@ procedure Login(userId: string; taskId: string; urlCode: string);
procedure LoginError(AMsg: string); procedure LoginError(AMsg: string);
begin begin
if (Pos('401:', AMsg) = 1) or
(Pos('Invalid url parameters', AMsg) > 0) or
(Pos('Expired link', AMsg) > 0) then
DisplayLoginView(userId, taskId)
else
ShowAppDialog('' + AMsg); ShowAppDialog('' + AMsg);
end; end;
begin begin
AuthService.Login( userId, taskId, urlCode, AuthService.Login(userId, taskId, urlCode,
@LoginSuccess, @LoginSuccess,
@LoginError @LoginError
); );
end; end;
procedure DisplayLoginView(AUserId: string; ATaskId: string; AMessage: string);
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
TFViewLogin.Display(@DisplayMainView, AUserId, ATaskId, AMessage);
end;
procedure DoLogin(); procedure DoLogin();
var var
userIdParam: string; userIdParam: string;
taskIdParam: string; taskIdParam: string;
codeParam: string; codeParam: string;
timeEntriesParam: string;
isTimeEntriesMode: Boolean;
begin begin
timeEntriesParam := Application.Parameters.Values['time_entries'];
isTimeEntriesMode :=
SameText(timeEntriesParam, 'true') or
(Application.Parameters.Values['task_id'] = '');
if isTimeEntriesMode then
begin
if AuthService.Authenticated and not AuthService.TokenExpired then
begin
DisplayMainView;
Exit;
end;
DisplayLoginView('', '', '');
Exit;
end;
userIdParam := Application.Parameters.Values['user_id']; userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id']; taskIdParam := Application.Parameters.Values['task_id'];
codeParam := Application.Parameters.Values['url_code']; codeParam := Application.Parameters.Values['url_code'];
if (userIdParam = '') or (taskIdParam = '') or (codeParam = '') then
begin
DisplayLoginView(userIdParam, taskIdParam);
end
else
begin
AuthService.Logout; AuthService.Logout;
DMConnection.ApiConnection.Connected := False; DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then if Assigned(FViewMain) then
FViewMain.Free; FViewMain.Free;
Login( userIdParam, taskIdParam, codeParam ); DMConnection.currentTaskId := taskIdParam;
Login(userIdParam, taskIdParam, codeParam);
end;
end; end;
procedure DoLogout(AMsg: string); procedure DoLogout(AMsg: string);
var
userIdParam: string;
taskIdParam: string;
timeEntriesParam: string;
isTimeEntriesMode: Boolean;
begin begin
AuthService.Logout; AuthService.Logout;
ShowAppDialog('Logout successful ' + AMsg);
end;
timeEntriesParam := Application.Parameters.Values['time_entries'];
isTimeEntriesMode :=
SameText(timeEntriesParam, 'true') or
(Application.Parameters.Values['task_id'] = '');
if isTimeEntriesMode then
begin
DisplayLoginView('', '', AMsg);
Exit;
end;
userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id'];
DisplayLoginView(userIdParam, taskIdParam, AMsg);
end;
procedure UnauthorizedAccessProc(AMessage: string); procedure UnauthorizedAccessProc(AMessage: string);
begin begin
ShowAppDialog('UnauthorizedAccessProc: ' + AMessage); ShowAppDialog('UnauthorizedAccessProc: ' + AMessage);
end; end;
procedure StartApplication; procedure StartApplication;
var var
ClientVer: string; ClientVer: string;
......
...@@ -94,15 +94,15 @@ ...@@ -94,15 +94,15 @@
<DCC_RemoteDebug>false</DCC_RemoteDebug> <DCC_RemoteDebug>false</DCC_RemoteDebug>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo> <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.8.8.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.8.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.3.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.8.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebBrowser>5</TMSWebBrowser>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <VerInfo_Release>3</VerInfo_Release>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath> <TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
<TMSWebSingleInstance>1</TMSWebSingleInstance>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols> <DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
...@@ -139,15 +139,29 @@ ...@@ -139,15 +139,29 @@
<DCCReference Include="Utils.pas"/> <DCCReference Include="Utils.pas"/>
<DCCReference Include="View.TaskItems.pas"> <DCCReference Include="View.TaskItems.pas">
<Form>FTaskItems</Form> <Form>FTaskItems</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="uNameOffCanvas.pas"/> <DCCReference Include="uNameOffCanvas.pas"/>
<DCCReference Include="uDropdownHelpers.pas"/> <DCCReference Include="uDropdownHelpers.pas"/>
<DCCReference Include="View.Login.pas">
<Form>FViewLogin</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.ErrorPage.pas">
<Form>FViewErrorPage</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TimeEntries.pas">
<Form>FTimeEntries</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uTaskPickerOffCanvas.pas"/>
<None Include="index.html"/> <None Include="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="config\config.json"/> <None Include="config\config.json"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
<None Include="css\task-items.css"/>
<None Include="css\time-entries.css"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
...@@ -228,6 +242,18 @@ ...@@ -228,6 +242,18 @@
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
<DeployFile LocalName="css\task-items.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\time-entries.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"/> <DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"> <DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32"> <Platform Name="Win32">
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" 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="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/app.css" rel="stylesheet"/>
<link href="css/task-items.css" rel="stylesheet"/>
<link href="css/time-entries.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet"/> <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> <script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
...@@ -17,7 +19,7 @@ ...@@ -17,7 +19,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="$(ProjectName).js"></script> <script src="$(ProjectName).js"></script>
</head> <link href="css/task-items.css" rel="stylesheet"/><link href="css/time-entries.css" rel="stylesheet"/></head>
<body> <body>
<noscript>Your browser does not support JavaScript!</noscript> <noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script> <script>rtl.run();</script>
...@@ -27,3 +29,5 @@ ...@@ -27,3 +29,5 @@
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.
object ApiDatabase: TApiDatabase object ApiDatabase: TApiDatabase
OnCreate = DataModuleCreate OnCreate = DataModuleCreate
Height = 475 Height = 632
Width = 996 Width = 994
object ucETaskApi: TUniConnection object ucETaskApi: TUniConnection
AutoCommit = False AutoCommit = False
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root'
Server = '192.168.102.133'
LoginPrompt = False LoginPrompt = False
Left = 267 Left = 435
Top = 395 Top = 359
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF' EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end end
object MySQLUniProvider1: TMySQLUniProvider object MySQLUniProvider1: TMySQLUniProvider
Left = 416 Left = 546
Top = 398 Top = 356
end end
object uqUsers: TUniQuery object uqUsers: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
...@@ -142,7 +144,7 @@ object ApiDatabase: TApiDatabase ...@@ -142,7 +144,7 @@ object ApiDatabase: TApiDatabase
'from task_items' 'from task_items'
'where TASK_ID = :TASK_ID' 'where TASK_ID = :TASK_ID'
'order by ITEM_NUM') 'order by ITEM_NUM')
Left = 56 Left = 58
Top = 26 Top = 26
ParamData = < ParamData = <
item item
...@@ -282,7 +284,7 @@ object ApiDatabase: TApiDatabase ...@@ -282,7 +284,7 @@ object ApiDatabase: TApiDatabase
'left join project p' 'left join project p'
' on p.PROJECT_ID = t.PROJECT_ID' ' on p.PROJECT_ID = t.PROJECT_ID'
'where t.TASK_ID = :TASK_ID') 'where t.TASK_ID = :TASK_ID')
Left = 54 Left = 60
Top = 82 Top = 82
ParamData = < ParamData = <
item item
...@@ -367,7 +369,7 @@ object ApiDatabase: TApiDatabase ...@@ -367,7 +369,7 @@ object ApiDatabase: TApiDatabase
'from task_items' 'from task_items'
'where TASK_ITEM_ID = :TASK_ITEM_ID' 'where TASK_ITEM_ID = :TASK_ITEM_ID'
' and TASK_ID = :TASK_ID') ' and TASK_ID = :TASK_ID')
Left = 240 Left = 236
Top = 16 Top = 16
ParamData = < ParamData = <
item item
...@@ -1085,4 +1087,277 @@ object ApiDatabase: TApiDatabase ...@@ -1085,4 +1087,277 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqAddTimeEntry: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert into time_items ('
' ENTRY_ID,'
' USER_ID,'
' TASK_DATE,'
' HOURS,'
' TASK_TIME,'
' CATEGORY,'
' SUMMARY,'
' CREATE_DATE,'
' CREATED_BY,'
' MODIFY_DATE,'
' MODIFIED_BY'
') values ('
' :ENTRY_ID,'
' :USER_ID,'
' :TASK_DATE,'
' null,'
' null,'
' null,'
' null,'
' now(),'
' :CREATED_BY,'
' now(),'
' :MODIFIED_BY'
')')
Left = 436
Top = 426
ParamData = <
item
DataType = ftUnknown
Name = 'ENTRY_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_DATE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CREATED_BY'
Value = nil
end
item
DataType = ftUnknown
Name = 'MODIFIED_BY'
Value = nil
end>
end
object uqSaveTimeEntry: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'UPDATE time_items'
'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(),'
' MODIFIED_BY = :MODIFIED_BY'
'WHERE ENTRY_ID = :ENTRY_ID'
' AND USER_ID = :USER_ID')
Left = 544
Top = 424
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_DATE'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'HOURS'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_TIME'
Value = nil
end
item
DataType = ftUnknown
Name = 'PLACE'
Value = nil
end
item
DataType = ftUnknown
Name = 'CATEGORY'
Value = nil
end
item
DataType = ftUnknown
Name = 'SUMMARY'
Value = nil
end
item
DataType = ftUnknown
Name = 'MODIFIED_BY'
Value = nil
end
item
DataType = ftUnknown
Name = 'ENTRY_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end>
end
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 end
...@@ -88,6 +88,25 @@ type ...@@ -88,6 +88,25 @@ type
uqRenameTaskItemReportedBy: TUniQuery; uqRenameTaskItemReportedBy: TUniQuery;
uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField; uqProjectReportedUsersTASK_ITEM_USER_ID: TStringField;
uqProjectReportedUsersNAME: TStringField; 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 DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -53,7 +53,7 @@ uses ...@@ -53,7 +53,7 @@ uses
XData.Sys.Exceptions, XData.Sys.Exceptions,
Common.Logging, Common.Logging,
Common.Middleware.Logging, Common.Middleware.Logging,
Common.Config, Vcl.Forms, IniFiles, Api.Service; Common.Config, Vcl.Forms, IniFiles, TaskItem.Service, TimeEntry.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'} {%CLASSGROUP 'Vcl.Controls.TControl'}
......
object AuthDatabase: TAuthDatabase object AuthDatabase: TAuthDatabase
OnCreate = DataModuleCreate OnCreate = DataModuleCreate
OnDestroy = DataModuleDestroy OnDestroy = DataModuleDestroy
Height = 202 Height = 236
Width = 578 Width = 327
object uqWebTasksUrl: TUniQuery object uqWebTasksUrl: TUniQuery
Connection = ucETaskAuth Connection = ucETaskAuth
SQL.Strings = ( SQL.Strings = (
...@@ -103,10 +103,9 @@ object AuthDatabase: TAuthDatabase ...@@ -103,10 +103,9 @@ object AuthDatabase: TAuthDatabase
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.102.131' Server = '192.168.102.133'
Connected = True
LoginPrompt = False LoginPrompt = False
Left = 69 Left = 71
Top = 133 Top = 133
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF' EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end end
...@@ -128,18 +127,19 @@ object AuthDatabase: TAuthDatabase ...@@ -128,18 +127,19 @@ object AuthDatabase: TAuthDatabase
' u.PERSPECTIVE_ID,' ' u.PERSPECTIVE_ID,'
' u.LAST_NAME,' ' u.LAST_NAME,'
' u.FIRST_NAME,' ' u.FIRST_NAME,'
' u.PASSWORD,'
' w.WEB_LOGIN' ' w.WEB_LOGIN'
'from web_tasks_url w' 'from web_tasks_url w'
'join users u on u.USER_ID = w.USER_ID' 'join users u on u.USER_ID = w.USER_ID'
'where w.USER_ID = :USER_ID' 'where u.USER_NAME = :USER_NAME'
' and w.TASK_ID = :TASK_ID' ' and w.TASK_ID = :TASK_ID'
' and u.PASSWORD = :PASSWORD') ' and w.URL_CODE = :URL_CODE')
Left = 192 Left = 194
Top = 44 Top = 44
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'USER_ID' Name = 'USER_NAME'
Value = nil Value = nil
end end
item item
...@@ -149,7 +149,7 @@ object AuthDatabase: TAuthDatabase ...@@ -149,7 +149,7 @@ object AuthDatabase: TAuthDatabase
end end
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'PASSWORD' Name = 'URL_CODE'
Value = nil Value = nil
end> end>
object uqWebLoginUSER_ID: TStringField object uqWebLoginUSER_ID: TStringField
...@@ -192,6 +192,10 @@ object AuthDatabase: TAuthDatabase ...@@ -192,6 +192,10 @@ object AuthDatabase: TAuthDatabase
FieldName = 'FIRST_NAME' FieldName = 'FIRST_NAME'
Size = 25 Size = 25
end end
object uqWebLoginPASSWORD: TStringField
FieldName = 'PASSWORD'
Size = 12
end
object uqWebLoginWEB_LOGIN: TStringField object uqWebLoginWEB_LOGIN: TStringField
FieldName = 'WEB_LOGIN' FieldName = 'WEB_LOGIN'
ReadOnly = True ReadOnly = True
......
...@@ -36,6 +36,7 @@ type ...@@ -36,6 +36,7 @@ type
uqWebLoginPERSPECTIVE_ID: TStringField; uqWebLoginPERSPECTIVE_ID: TStringField;
uqWebLoginLAST_NAME: TStringField; uqWebLoginLAST_NAME: TStringField;
uqWebLoginFIRST_NAME: TStringField; uqWebLoginFIRST_NAME: TStringField;
uqWebLoginPASSWORD: TStringField;
uqWebLoginWEB_LOGIN: TStringField; uqWebLoginWEB_LOGIN: TStringField;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject); procedure DataModuleDestroy(Sender: TObject);
......
...@@ -18,9 +18,9 @@ type ...@@ -18,9 +18,9 @@ type
[ServiceContract, Model(AUTH_MODEL)] [ServiceContract, Model(AUTH_MODEL)]
IAuthService = interface(IInvokable) IAuthService = interface(IInvokable)
['{9CFD59B2-A832-4F82-82BB-9A25FC93F305}'] ['{9CFD59B2-A832-4F82-82BB-9A25FC93F305}']
function Login(const userId, taskId, urlCode: string): string; function Login(userId, taskId, urlCode: string): string;
function VerifyVersion(ClientVersion: string): TJSONObject; function VerifyVersion(ClientVersion: string): TJSONObject;
function WebLogin(const userId, taskId, password: string): string; function WebLogin(userName, loginType, loginValue, password: string): string;
end; end;
implementation implementation
......
unit Main; unit Main;
//Authors:
//Elias Sarraf
//Mac Stephens
//Cameron Hayes
interface interface
uses uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi, Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles, Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles,
Auth.Service, Auth.Server.Module, Api.Service, Api.Server.Module, App.Server.Module, TaskItem.Service, Auth.Server.Module, Auth.Service, Api.Server.Module, App.Server.Module,
ExeInfo; TimeEntry.Service, TimeEntry.ServiceImpl, ExeInfo;
type type
TFMain = class(TForm) TFMain = class(TForm)
......
unit Api.Service; unit TaskItem.Service;
interface interface
...@@ -115,10 +115,10 @@ type ...@@ -115,10 +115,10 @@ type
type type
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable) ITaskItemService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}'] ['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
function GetTaskItems(taskId: string): TTaskItemsResponse; [HttpGet] function GetTaskItems(taskId: string): TTaskItemsResponse;
[HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean; [HttpPost] function AddTaskRow(taskId: string; insertAfterItemNum: Integer): Boolean;
[HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse; [HttpPost] function AddAssignedName(taskId: string; name: string): TTaskUserOptionsResponse;
...@@ -134,9 +134,10 @@ type ...@@ -134,9 +134,10 @@ type
[HttpPost] function RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse; [HttpPost] function RenameApplicationName(taskId: string; oldName: string; newName: string): TTaskApplicationOptionsResponse;
[HttpPost] function DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse; [HttpPost] function DeleteApplicationName(taskId: string; name: string): TTaskApplicationOptionsResponse;
procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer); [HttpPost] procedure MoveTaskRow(const taskId: Integer; const taskItemId: Integer; const newItemNum: Integer);
function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean; [HttpPost] function DeleteTaskRow(const taskId: Integer; const taskItemId: Integer): Boolean;
function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean; [HttpPost] function SaveTaskItemField(const Item: TTaskItemFieldSave): Boolean;
end; end;
implementation implementation
...@@ -188,6 +189,6 @@ begin ...@@ -188,6 +189,6 @@ begin
end; end;
initialization initialization
RegisterServiceType(TypeInfo(IApiService)); RegisterServiceType(TypeInfo(ITaskItemService));
end. end.
unit TimeEntry.Service;
interface
uses
System.Generics.Collections,
XData.Service.Common,
Bcl.Types.Nullable,
Aurelius.Mapping.Attributes;
const
API_MODEL = 'Api';
type
TTimeEntry = class
public
entryId: Integer;
taskDate: string;
taskId: string;
taskDisplay: string;
hours: Nullable<Double>;
taskTime: string;
place: string;
placeDesc: string;
category: string;
categoryDesc: string;
summary: string;
end;
TTimeEntryTaskOption = class
public
taskId: string;
taskDisplay: string;
end;
TTimeEntryCategoryOption = class
public
code: string;
codeDesc: string;
end;
TTimeEntriesResponse = class
public
userName: string;
count: Integer;
items: TList<TTimeEntry>;
taskOptions: TList<TTimeEntryTaskOption>;
categoryOptions: TList<TTimeEntryCategoryOption>;
placeOptions: TList<TTimeEntryCategoryOption>;
constructor Create;
destructor Destroy; override;
end;
TTimeEntrySave = class
public
entryId: string;
userId: string;
taskDate: string;
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): string;
[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
constructor TTimeEntriesResponse.Create;
begin
inherited;
items := TList<TTimeEntry>.Create;
taskOptions := TList<TTimeEntryTaskOption>.Create;
categoryOptions := TList<TTimeEntryCategoryOption>.Create;
placeOptions := TList<TTimeEntryCategoryOption>.Create;
end;
destructor TTimeEntriesResponse.Destroy;
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;
initialization
RegisterServiceType(TypeInfo(ITimeEntryService));
end.
object AuthDatabase: TAuthDatabase
OnCreate = DataModuleCreate
OnDestroy = DataModuleDestroy
Height = 207
Width = 303
object uqWebTasksUrl: TUniQuery
Connection = ucETaskAuth
SQL.Strings = (
'select'
' u.USER_ID,'
' u.USER_NAME,'
' u.NAME,'
' u.STATUS,'
' u.EMAIL,'
' u.ACCESS_LEVEL,'
' u.TASK_RIGHTS,'
' u.PERSPECTIVE_ID,'
' u.LAST_NAME,'
' u.FIRST_NAME,'
' w.URL_TIME,'
' w.URL_TIME_EXP'
'from web_tasks_url w'
'join users u on u.USER_ID = w.USER_ID'
'where w.USER_ID = :USER_ID'
' and w.TASK_ID = :TASK_ID'
' and w.URL_CODE = :URL_CODE'
' and TIMESTAMPDIFF(SECOND, w.URL_TIME, NOW()) between 0 and w.U' +
'RL_TIME_EXP'
'order by w.URL_TIME desc'
'limit 1')
FetchRows = 100
Left = 78
Top = 43
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'URL_CODE'
Value = nil
end>
object uqWebTasksUrlUSER_ID: TStringField
FieldName = 'USER_ID'
Required = True
Size = 7
end
object uqWebTasksUrlUSER_NAME: TStringField
FieldName = 'USER_NAME'
Required = True
Size = 12
end
object uqWebTasksUrlNAME: TStringField
FieldName = 'NAME'
Size = 40
end
object uqWebTasksUrlSTATUS: TStringField
FieldName = 'STATUS'
Size = 7
end
object uqWebTasksUrlEMAIL: TStringField
FieldName = 'EMAIL'
Size = 50
end
object uqWebTasksUrlACCESS_LEVEL: TIntegerField
FieldName = 'ACCESS_LEVEL'
end
object uqWebTasksUrlTASK_RIGHTS: TIntegerField
FieldName = 'TASK_RIGHTS'
end
object uqWebTasksUrlPERSPECTIVE_ID: TStringField
FieldName = 'PERSPECTIVE_ID'
Size = 45
end
object uqWebTasksUrlLAST_NAME: TStringField
FieldName = 'LAST_NAME'
Size = 35
end
object uqWebTasksUrlFIRST_NAME: TStringField
FieldName = 'FIRST_NAME'
Size = 25
end
object uqWebTasksUrlURL_TIME: TDateTimeField
FieldName = 'URL_TIME'
ReadOnly = True
Required = True
end
object uqWebTasksUrlURL_TIME_EXP: TIntegerField
FieldName = 'URL_TIME_EXP'
ReadOnly = True
Required = True
end
end
object ucETaskAuth: TUniConnection
ProviderName = 'MySQL'
Database = 'eTask'
Username = 'root'
Server = '192.168.102.131'
Connected = True
LoginPrompt = False
Left = 69
Top = 133
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 194
Top = 132
end
object uqWebLogin: TUniQuery
Connection = ucETaskAuth
SQL.Strings = (
'select'
' u.USER_ID,'
' u.USER_NAME,'
' u.NAME,'
' u.STATUS,'
' u.EMAIL,'
' u.ACCESS_LEVEL,'
' u.TASK_RIGHTS,'
' u.PERSPECTIVE_ID,'
' u.LAST_NAME,'
' u.FIRST_NAME,'
' w.WEB_LOGIN'
'from web_tasks_url w'
'join users u on u.USER_ID = w.USER_ID'
'where w.USER_ID = :USER_ID'
' and w.TASK_ID = :TASK_ID'
' and u.PASSWORD = :PASSWORD')
Left = 192
Top = 44
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'PASSWORD'
Value = nil
end>
object uqWebLoginUSER_ID: TStringField
FieldName = 'USER_ID'
Required = True
Size = 7
end
object uqWebLoginUSER_NAME: TStringField
FieldName = 'USER_NAME'
Required = True
Size = 12
end
object uqWebLoginNAME: TStringField
FieldName = 'NAME'
Size = 40
end
object uqWebLoginSTATUS: TStringField
FieldName = 'STATUS'
Size = 7
end
object uqWebLoginEMAIL: TStringField
FieldName = 'EMAIL'
Size = 50
end
object uqWebLoginACCESS_LEVEL: TIntegerField
FieldName = 'ACCESS_LEVEL'
end
object uqWebLoginTASK_RIGHTS: TIntegerField
FieldName = 'TASK_RIGHTS'
end
object uqWebLoginPERSPECTIVE_ID: TStringField
FieldName = 'PERSPECTIVE_ID'
Size = 45
end
object uqWebLoginLAST_NAME: TStringField
FieldName = 'LAST_NAME'
Size = 35
end
object uqWebLoginFIRST_NAME: TStringField
FieldName = 'FIRST_NAME'
Size = 25
end
object uqWebLoginWEB_LOGIN: TStringField
FieldName = 'WEB_LOGIN'
ReadOnly = True
Required = True
FixedChar = True
Size = 1
end
end
end
// Auth Database to verify logins
unit Auth.Database;
interface
uses
System.SysUtils, System.Classes, IniFiles, Vcl.Forms, MemDS,
Data.DB, DBAccess, Uni, UniProvider, PostgreSQLUniProvider, MySQLUniProvider;
type
TAuthDatabase = class(TDataModule)
uqWebTasksUrl: TUniQuery;
ucETaskAuth: TUniConnection;
MySQLUniProvider1: TMySQLUniProvider;
uqWebTasksUrlUSER_ID: TStringField;
uqWebTasksUrlUSER_NAME: TStringField;
uqWebTasksUrlNAME: TStringField;
uqWebTasksUrlSTATUS: TStringField;
uqWebTasksUrlEMAIL: TStringField;
uqWebTasksUrlACCESS_LEVEL: TIntegerField;
uqWebTasksUrlTASK_RIGHTS: TIntegerField;
uqWebTasksUrlPERSPECTIVE_ID: TStringField;
uqWebTasksUrlLAST_NAME: TStringField;
uqWebTasksUrlFIRST_NAME: TStringField;
uqWebTasksUrlURL_TIME: TDateTimeField;
uqWebTasksUrlURL_TIME_EXP: TIntegerField;
uqWebLogin: TUniQuery;
uqWebLoginUSER_ID: TStringField;
uqWebLoginUSER_NAME: TStringField;
uqWebLoginNAME: TStringField;
uqWebLoginSTATUS: TStringField;
uqWebLoginEMAIL: TStringField;
uqWebLoginACCESS_LEVEL: TIntegerField;
uqWebLoginTASK_RIGHTS: TIntegerField;
uqWebLoginPERSPECTIVE_ID: TStringField;
uqWebLoginLAST_NAME: TStringField;
uqWebLoginFIRST_NAME: TStringField;
uqWebLoginWEB_LOGIN: TStringField;
procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
AuthDatabase: TAuthDatabase;
implementation
uses
System.JSON,
Common.Config,
Common.Logging,
uLibrary;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
procedure TAuthDatabase.DataModuleCreate(Sender: TObject);
begin
Logger.Log( 5, 'TAuthDatabase.DataModuleCreate' );
LoadDatabaseSettings( ucETaskAuth, 'emT3XDataServer.ini' );
try
ucETaskAuth.Connect;
except
on E: Exception do
begin
Logger.Log(2, '--TAuthDatabase.DataModuleCreate -Error connecting to database: ' + E.Message);
end;
end;
end;
procedure TAuthDatabase.DataModuleDestroy(Sender: TObject);
begin
ucETaskAuth.Connected := false;
end;
end.
[Auth.ServiceImpl.pas]
SaveTime=4/15/2026 6:21:59 PM
FileCount=1
File0=C:\Projects\emT3web\emT3XDataServer\Source\Auth.ServiceImpl.pas
[Auth.Database.pas]
SaveTime=4/15/2026 6:21:59 PM
FileCount=2
File0=C:\Projects\emT3web\emT3XDataServer\Source\Auth.Database.pas
File1=C:\Projects\emT3web\emT3XDataServer\Source\Auth.Database.dfm
[Settings] [Settings]
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.8.8 webClientVersion=0.9.3
LogFileNum=161 LogFileNum=245
[Database] [Database]
Server=192.168.102.131 Server=192.168.102.133
--Server=192.168.116.128 --Server=192.168.116.131
--Server=192.168.102.131
--Server=192.168.159.10 --Server=192.168.159.10
Database=eTask Database=eTask
Username=root Username=root
......
...@@ -20,9 +20,11 @@ uses ...@@ -20,9 +20,11 @@ uses
Auth.Service in 'Source\Auth.Service.pas', Auth.Service in 'Source\Auth.Service.pas',
Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas', Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas',
App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule}, App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule},
Api.Service in 'Source\Api.Service.pas', TaskItem.Service in 'Source\TaskItem.Service.pas',
Api.ServiceImpl in 'Source\Api.ServiceImpl.pas', TaskItem.ServiceImpl in 'Source\TaskItem.ServiceImpl.pas',
Common.Ini in 'Source\Common.Ini.pas'; Common.Ini in 'Source\Common.Ini.pas',
TimeEntry.Service in 'Source\TimeEntry.Service.pas',
TimeEntry.ServiceImpl in 'Source\TimeEntry.ServiceImpl.pas';
type type
TMemoLogAppender = class( TInterfacedObject, ILogAppender ) TMemoLogAppender = class( TInterfacedObject, ILogAppender )
......
...@@ -114,10 +114,12 @@ ...@@ -114,10 +114,12 @@
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<DCC_ExeOutput>.\bin</DCC_ExeOutput> <DCC_ExeOutput>.\bin</DCC_ExeOutput>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> <DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.8.8.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys> <VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.9.3.1;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release> <VerInfo_AutoIncVersion>true</VerInfo_AutoIncVersion>
<VerInfo_Build>1</VerInfo_Build>
<VerInfo_Release>3</VerInfo_Release>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''"> <PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
...@@ -175,9 +177,11 @@ ...@@ -175,9 +177,11 @@
<Form>AppServerModule</Form> <Form>AppServerModule</Form>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="Source\Api.Service.pas"/> <DCCReference Include="Source\TaskItem.Service.pas"/>
<DCCReference Include="Source\Api.ServiceImpl.pas"/> <DCCReference Include="Source\TaskItem.ServiceImpl.pas"/>
<DCCReference Include="Source\Common.Ini.pas"/> <DCCReference Include="Source\Common.Ini.pas"/>
<DCCReference Include="Source\TimeEntry.Service.pas"/>
<DCCReference Include="Source\TimeEntry.ServiceImpl.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment