Commit d8438015 by Mac Stephens

expand complaint details data model and endpoints (updated uqComplaintDetails,…

expand complaint details data model and endpoints (updated uqComplaintDetails, added history/contacts/warnings queries + endpoints) and refactor client ComplaintDetails to tabbed DB tables (remarks/history/contacts/warnings) with memo-type toggle filters. Fix navigation/JS bridge routing via View.Main, correct form injection/scroll behavior, and add sticky summary + controls with loading spinners for tab fetches.
parent 5a8f87eb
...@@ -21,3 +21,5 @@ emiMobileServer/logs/* ...@@ -21,3 +21,5 @@ emiMobileServer/logs/*
*.tvsconfig *.tvsconfig
*.dxsettings *.dxsettings
*.zip
...@@ -10,7 +10,6 @@ uses ...@@ -10,7 +10,6 @@ uses
type type
TApiDatabaseModule = class(TDataModule) TApiDatabaseModule = class(TDataModule)
OracleUniProvider1: TOracleUniProvider; OracleUniProvider1: TOracleUniProvider;
uqBooking: TUniQuery;
uqMapUnits: TUniQuery; uqMapUnits: TUniQuery;
uqUnitList: TUniQuery; uqUnitList: TUniQuery;
uqComplaintUnits: TUniQuery; uqComplaintUnits: TUniQuery;
...@@ -131,6 +130,20 @@ type ...@@ -131,6 +130,20 @@ type
uqMapUnitsCALL_TYPE: TStringField; uqMapUnitsCALL_TYPE: TStringField;
uqMapUnitsPRIORITY: TStringField; uqMapUnitsPRIORITY: TStringField;
uqMapUnitsUNIT_STATUS_DESC: TStringField; uqMapUnitsUNIT_STATUS_DESC: TStringField;
uqComplaintDetailsHISTORY: TFloatField;
uqComplaintDetailsCONTACTS: TFloatField;
uqComplaintDetailsWARNINGS: TFloatField;
uqComplaintDetailsADDRESSID: TFloatField;
uqComplaintDetailsAGENCY: TStringField;
uqComplaintDetailsSTRNUMBER: TFloatField;
uqComplaintDetailsSTRHNUMBER: TStringField;
uqComplaintDetailsSTRPREFIX: TStringField;
uqComplaintDetailsSTRNAME: TStringField;
uqComplaintDetailsSTRSUFFIX: TStringField;
uqComplaintDetailsCITY: TStringField;
uqComplaintHistory: TUniQuery;
uqComplaintContacts: TUniQuery;
uqComplaintWarnings: TUniQuery;
procedure uqComplaintListCalcFields(DataSet: TDataSet); procedure uqComplaintListCalcFields(DataSet: TDataSet);
procedure uqMapComplaintsCalcFields(DataSet: TDataSet); procedure uqMapComplaintsCalcFields(DataSet: TDataSet);
private private
......
...@@ -22,6 +22,11 @@ type ...@@ -22,6 +22,11 @@ type
[HttpGet] function GetComplaintMap: TJSONObject; [HttpGet] function GetComplaintMap: TJSONObject;
[HttpGet] function GetUnitMap: TJSONObject; [HttpGet] function GetUnitMap: TJSONObject;
[HttpGet] function GetComplaintDetails(const ComplaintId: string): TJSONObject; [HttpGet] function GetComplaintDetails(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintMemos(const CfsId: string): TJSONObject;
[HttpGet] function GetComplaintHistory(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintContacts(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintWarnings(const ComplaintId: string): TJSONObject;
end; end;
implementation implementation
......
[Settings] [Settings]
LogFileNum=533 LogFileNum=551
webClientVersion=0.1.0 webClientVersion=0.1.0
<div class="row"> <div class="container-fluid p-3">
<div class="col-lg-12">
<h1 class="page-header" id="view.userprofile.title">Admin User Profile</h1>
<div role="form"> <h1 id="view.userprofile.title" class="h2 border-bottom pb-2 mb-4 text-primary">
<div class="form-group"> Admin User Profile
<label id="view.userprofile.form.lblUserName">User Name:</label> </h1>
<input id="view.userprofile.form.edtUserName" class="form-control">
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title text-secondary mb-3">User Details</h5>
<div class="row g-3">
<div class="col-12">
<label id="view.userprofile.form.lblFullName" for="view.userprofile.form.edtFullName" class="form-label fw-bold">User Fullname:</label>
<input id="view.userprofile.form.edtFullName" type="text" class="form-control">
</div> </div>
<div class="form-group">
<label id="view.userprofile.form.lblFullName">User Fullname:</label> <div class="col-md-6">
<input id="view.userprofile.form.edtFullName" class="form-control"> <label id="view.userprofile.form.lblUserName" for="view.userprofile.form.edtUserName" class="form-label fw-bold">User Name:</label>
<input id="view.userprofile.form.edtUserName" type="text" class="form-control">
</div> </div>
<div class="form-group"> <div class="col-md-6">
<label id="view.userprofile.form.lblAgency">User Agency:</label> <label id="view.userprofile.form.lblAgency" for="view.userprofile.form.edtAgency" class="form-label fw-bold">User Agency:</label>
<input id="view.userprofile.form.edtAgency" class="form-control"> <input id="view.userprofile.form.edtAgency" type="text" class="form-control">
</div> </div>
<div class="form-group">
<label id="view.userprofile.form.lblBadgeNum">User Bage #:</label> <div class="col-md-4">
<input id="view.userprofile.form.edtBadgeNum" class="form-control"> <label id="view.userprofile.form.lblBadgeNum" for="view.userprofile.form.edtBadgeNum" class="form-label small text-muted">User Badge #:</label>
<input id="view.userprofile.form.edtBadgeNum" type="text" class="form-control">
</div> </div>
<div class="form-group"> <div class="col-md-4">
<label id="view.userprofile.form.lblUserId">User Id:</label> <label id="view.userprofile.form.lblUserId" for="view.userprofile.form.edtUserId" class="form-label small text-muted">User Id:</label>
<input id="view.userprofile.form.edtUserId" class="form-control"> <input id="view.userprofile.form.edtUserId" type="text" class="form-control">
</div> </div>
<div class="form-group"> <div class="col-md-4">
<label id="view.userprofile.form.lblPersonnelId">Personnel Id:</label> <label id="view.userprofile.form.lblPersonnelId" for="view.userprofile.form.edtPersonnelId" class="form-label small text-muted">Personnel Id:</label>
<input id="view.userprofile.form.edtPersonnelId" class="form-control"> <input id="view.userprofile.form.edtPersonnelId" type="text" class="form-control">
</div> </div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="view.userprofile.form.chkAdminUser"> <div class="col-12">
<label class="custom-control-label" for="view.userprofile.form.chkAdminUser">Admin User</label> <div class="form-check mt-2">
<input type="checkbox" class="form-check-input" id="view.userprofile.form.chkAdminUser">
<label class="form-check-label user-select-none" for="view.userprofile.form.chkAdminUser">
Admin User
</label>
</div>
</div>
</div> </div>
<div class="form-input">
<div><label id="lblinfo" class="py-2" style="font-size: 1.00rem;"></label></div>
<div><input class="form-control input-sm" id="edtusername" width='50%'/></div>
<div class="py-2"><input class="form-control input-sm" id="edtpassword" width='50%'/></div>
<button id="btnadduser"></button>
<div><label id="lblresult" class="py-2" style="font-size: 1.00rem;"></label></div>
</div> </div>
</div> </div>
<div class="card shadow-sm border-0 bg-light">
<div class="card-body">
<h6 class="card-subtitle mb-3 text-muted">Quick Actions / Create User</h6>
<div class="mb-2" style="min-height: 1.5rem;">
<label id="lblinfo" class="text-info fw-bold mb-0"></label>
</div> </div>
<div class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Username</label>
<input id="edtusername" type="text" class="form-control form-control-sm">
</div>
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Password</label>
<input id="edtpassword" type="password" class="form-control form-control-sm">
</div>
<div class="col-md-2">
<button id="btnadduser" class="btn btn-primary btn-sm w-100">
<i class="fa fa-plus me-1"></i> Add
</button>
</div>
</div>
<div class="mt-2">
<label id="lblresult" class="text-success small mb-0"></label>
</div>
</div>
</div>
</div> </div>
<!-- Sticky local navbar (Complaint Details) --> <div class="d-flex flex-column h-100 w-100 overflow-hidden">
<div class="sticky-top"> <div class="flex-grow-1 d-flex flex-column overflow-auto bg-light p-2 p-md-3" style="min-height:0;">
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid"> <!-- Sticky block: Summary header + (expanded) summary body + buttons -->
<div class="row w-100 g-2 align-items-stretch"> <div class="sticky-top bg-light" style="z-index:20;">
<div class="col">
<div id="cdetails_title" class="navbar-brand mb-0 h5 text-white">Complaint Details</div> <!-- Summary header -->
</div> <div class="card border-0 shadow-sm mb-2">
<div class="col-auto"> <div class="card-header bg-white py-2">
<button id="btn_close_complaint_details" type="button" class="btn btn-outline-light w-100 h-100"> <button
<i class="fa fa-times me-1"></i> class="btn btn-link text-decoration-none p-0 w-100 d-flex align-items-center justify-content-between"
<span class="d-none d-sm-inline">Close</span> type="button"
data-bs-toggle="collapse"
data-bs-target="#cdetails_summary"
aria-expanded="false"
aria-controls="cdetails_summary">
<span class="fw-semibold text-dark" id="lbl_summary_title">Summary</span>
<i class="fa fa-chevron-down"></i>
</button> </button>
</div> </div>
</div> </div>
</div>
</nav>
</div>
<!-- Complaint details content --> <!-- Summary body (stays sticky because it's inside the sticky block) -->
<div class="container-fluid mt-2 complaint-details-page"> <div id="cdetails_summary" class="collapse">
<div class="row justify-content-center"> <div class="card border-0 shadow-sm mb-2">
<div class="col-12 col-md-10 col-lg-8">
<!-- Outer card -->
<div class="card border-0 shadow-sm">
<div class="card-body p-3 bg-light">
<!-- Summary panel -->
<div class="card mb-3">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="row gy-1"> <div class="row gy-1 gx-2">
<div class="col-5 col-sm-4 fw-semibold">Complaint</div> <div class="col-5 col-sm-4 fw-semibold text-muted">Priority:</div>
<div class="col-7 col-sm-8" id="lbl_complaint_number"></div>
<div class="col-5 col-sm-4 fw-semibold">Priority</div>
<div class="col-7 col-sm-8" id="lbl_priority"></div> <div class="col-7 col-sm-8" id="lbl_priority"></div>
<div class="col-5 col-sm-4 fw-semibold">Status</div> <div class="col-5 col-sm-4 fw-semibold text-muted">Status:</div>
<div class="col-7 col-sm-8" id="lbl_status"></div> <div class="col-7 col-sm-8" id="lbl_status"></div>
<div class="col-5 col-sm-4 fw-semibold">Dispatch Code</div> <div class="col-5 col-sm-4 fw-semibold text-muted">Dispatch Code:</div>
<div class="col-7 col-sm-8" id="lbl_dispatch_code"></div> <div class="col-7 col-sm-8" id="lbl_dispatch_code"></div>
<div class="col-5 col-sm-4 fw-semibold">Dispatch District</div> <div class="col-5 col-sm-4 fw-semibold text-muted">Dispatch District:</div>
<div class="col-7 col-sm-8" id="lbl_dispatch_district"></div> <div class="col-7 col-sm-8" id="lbl_dispatch_district"></div>
<div class="col-5 col-sm-4 fw-semibold">Address</div> <div class="col-5 col-sm-4 fw-semibold text-muted">Address:</div>
<div class="col-7 col-sm-8" id="lbl_address"></div> <div class="col-7 col-sm-8" id="lbl_address"></div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Action tabs (green buttons) --> <!-- Buttons -->
<div class="mb-3"> <div class="pb-2">
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2 justify-content-center">
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_remarks" aria-pressed="true">Remarks</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_history">History</button> <button type="button" class="btn btn-success btn-sm px-3" id="btn_history">History</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_warnings">Warnings</button> <button type="button" class="btn btn-success btn-sm px-3" id="btn_warnings">Warnings</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_contacts">Contacts</button> <button type="button" class="btn btn-success btn-sm px-3" id="btn_contacts">Contacts</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_cmp">CMP</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_e911">E-911</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_rem">REM</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_unt">UNT</button>
</div>
</div> </div>
<!-- Details table (thead in HTML, tbody filled by TWebDBListControl) --> <!-- Remarks toggle filters (only visible on Remarks tab) -->
<div class="card"> <div class="d-flex flex-wrap gap-2 justify-content-center mt-2" id="row_remarks_toggles">
<div class="card-body p-0"> <button type="button" class="btn btn-success btn-sm px-3 active" id="btn_cmp" aria-pressed="true">CMP</button>
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;"> <button type="button" class="btn btn-success btn-sm px-3 active" id="btn_e911" aria-pressed="true">E-911</button>
<table class="table table-sm table-striped mb-0"> <button type="button" class="btn btn-success btn-sm px-3 active" id="btn_rem" aria-pressed="true">REM</button>
<thead class="table-light sticky-top"> <button type="button" class="btn btn-success btn-sm px-3 active" id="btn_unt" aria-pressed="true">UNT</button>
<tr>
<th style="width: 70px;">Type</th>
<th style="width: 150px;">Timestamp</th>
<th>Remarks</th>
</tr>
</thead>
<tbody id="tbl_complaint_details"></tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
<!-- Tables (scroll with page) -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div id="tbl_remarks"></div>
<div id="tbl_history" class="d-none"></div>
<div id="tbl_contacts" class="d-none"></div>
<div id="tbl_warnings" class="d-none"></div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column vh-100">
<!-- Top Nav --> <!-- Top Nav -->
<nav class="navbar navbar-light bg-primary border-light text-light fixed-top"> <nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
<div class="container-fluid"> <div class="container-fluid">
<!-- Left: Font button --> <!-- Left: Font button -->
<button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm"> <button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm">
...@@ -12,17 +12,17 @@ ...@@ -12,17 +12,17 @@
<a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a> <a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a>
<!-- Right: Connection label --> <!-- Right: Connection label -->
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto">Connected</span> <span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span>
</div> </div>
</nav> </nav>
<!-- Main content: fills space between navbars --> <!-- Main content: fills space between navbars -->
<main id="main.webpanel" class="flex-grow-1 overflow-auto mt-5 mb-5"> <main id="main.webpanel" class="flex-grow-1 position-relative p-0 overflow-hidden" style="min-height:0;">
<!-- TWebPanel content gets injected here --> <!-- TWebPanel content gets injected here -->
</main> </main>
<!-- Bottom Nav --> <!-- Bottom Nav -->
<nav class="navbar navbar-dark bg-primary fixed-bottom py-2"> <nav class="navbar navbar-dark bg-primary py-2 flex-shrink-0">
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-center gap-3 w-100"> <div class="d-flex justify-content-center gap-3 w-100">
<button id="view.main.btnmap" type="button" class="btn btn-primary"> <button id="view.main.btnmap" type="button" class="btn btn-primary">
......
...@@ -44,8 +44,9 @@ type ...@@ -44,8 +44,9 @@ type
procedure ShowCrudForm( AFormClass: TWebFormClass ); procedure ShowCrudForm( AFormClass: TWebFormClass );
//procedure EditUser( AParam, BParam, CParam, DParam, EParam: string); //procedure EditUser( AParam, BParam, CParam, DParam, EParam: string);
function GetUserInfo: string; function GetUserInfo: string;
procedure SetActiveNavButton(const BtnId: string);
[async] procedure RefreshBadgesAsync; [async] procedure RefreshBadgesAsync;
procedure ShowUnitDetails(UnitId: string);
procedure SetHeaderTitle(const title: string);
public public
{ Public declarations } { Public declarations }
class procedure Display(LogoutProc: TLogoutProc); class procedure Display(LogoutProc: TLogoutProc);
...@@ -53,6 +54,7 @@ type ...@@ -53,6 +54,7 @@ type
procedure EditUser( Mode, FullName, Username, Phone, Email: string; admin, active: boolean); procedure EditUser( Mode, FullName, Username, Phone, Email: string; admin, active: boolean);
procedure ShowUserForm(Info: string); procedure ShowUserForm(Info: string);
procedure ShowComplaintDetails(ComplaintId: string); procedure ShowComplaintDetails(ComplaintId: string);
procedure SetActiveNavButton(const BtnId: string);
end; end;
var var
...@@ -71,6 +73,7 @@ uses ...@@ -71,6 +73,7 @@ uses
View.Admin, View.Admin,
View.Users, View.Users,
View.EditUser, View.EditUser,
View.UnitDetails,
Utils; Utils;
{$R *.dfm} {$R *.dfm}
...@@ -90,6 +93,15 @@ begin ...@@ -90,6 +93,15 @@ begin
RefreshBadgesAsync; RefreshBadgesAsync;
end; end;
procedure TFViewMain.SetHeaderTitle(const title: string);
var
el: TJSElement;
begin
el := Document.getElementById('view.main.lbltitle');
if el <> nil then
TJSHtmlElement(el).innerText := title;
end;
procedure TFViewMain.lblUsersClick(Sender: TObject); procedure TFViewMain.lblUsersClick(Sender: TObject);
begin begin
...@@ -136,6 +148,7 @@ end; ...@@ -136,6 +148,7 @@ end;
procedure TFViewMain.btnComplaintsClick(Sender: TObject); procedure TFViewMain.btnComplaintsClick(Sender: TObject);
begin begin
SetHeaderTitle('Complaints');
SetActiveNavButton('view.main.btncomplaints'); SetActiveNavButton('view.main.btncomplaints');
ShowForm(TFViewComplaints); ShowForm(TFViewComplaints);
...@@ -149,12 +162,14 @@ end; ...@@ -149,12 +162,14 @@ end;
procedure TFViewMain.btnMapClick(Sender: TObject); procedure TFViewMain.btnMapClick(Sender: TObject);
begin begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap'); SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap); ShowForm(TFViewMap);
end; end;
procedure TFViewMain.btnUnitsClick(Sender: TObject); procedure TFViewMain.btnUnitsClick(Sender: TObject);
begin begin
SetHeaderTitle('Units');
SetActiveNavButton('view.main.btnunits'); SetActiveNavButton('view.main.btnunits');
ShowForm(TFViewUnits); ShowForm(TFViewUnits);
end; end;
...@@ -197,11 +212,19 @@ end; ...@@ -197,11 +212,19 @@ end;
procedure TFViewMain.ShowComplaintDetails(ComplaintId: string); procedure TFViewMain.ShowComplaintDetails(ComplaintId: string);
begin begin
SetHeaderTitle('Complaint Details');
if Assigned(FChildForm) then if Assigned(FChildForm) then
FChildForm.Free; FChildForm.Free;
FChildForm := TFViewComplaintDetails.CreateForm(WebPanel1.ElementID, ComplaintId); FChildForm := TFViewComplaintDetails.CreateForm(WebPanel1.ElementID, ComplaintId);
end; end;
procedure TFViewMain.ShowUnitDetails(UnitId: string);
begin
SetHeaderTitle('Unit Details');
if Assigned(FChildForm) then
FChildForm.Free;
FChildForm := TFViewUnitDetails.CreateForm(WebPanel1.ElementID, UnitId);
end;
procedure TFViewMain.SetActiveNavButton(const btnId: string); procedure TFViewMain.SetActiveNavButton(const btnId: string);
......
...@@ -74,8 +74,8 @@ object FViewMap: TFViewMap ...@@ -74,8 +74,8 @@ object FViewMap: TFViewMap
Width = 335 Width = 335
Height = 555 Height = 555
ElementID = 'map_pnlmap' ElementID = 'map_pnlmap'
Caption = 'pnlMap'
ChildOrder = 7 ChildOrder = 7
ElementPosition = epIgnore
TabOrder = 6 TabOrder = 6
object lfMap: TTMSFNCLeaflet object lfMap: TTMSFNCLeaflet
Left = 0 Left = 0
...@@ -131,4 +131,10 @@ object FViewMap: TFViewMap ...@@ -131,4 +131,10 @@ object FViewMap: TFViewMap
Left = 358 Left = 358
Top = 696 Top = 696
end end
object tmrZoomToBounds: TWebTimer
Enabled = False
OnTimer = tmrZoomToBoundsTimer
Left = 358
Top = 750
end
end end
<!-- Root wrapper inside main.webpanel --> <div id="map.root" class="d-flex flex-column h-100 w-100">
<div id="map.root" class="d-flex flex-column" style="height:100%;">
<nav class="d-flex gap-2 bg-primary p-2 overflow-x-auto border-bottom border-primary shadow-sm flex-shrink-0">
<!-- Local navbar -->
<nav class="navbar navbar-dark bg-primary py-2"> <button id="map.btnmenu" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<div class="container-fluid"> <i class="fa fa-bars me-2"></i><span class="d-none d-sm-inline">Menu</span>
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<button id="map.btnmenu" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-bars me-1"></i><span class="d-none d-sm-inline">Menu</span>
</button> </button>
</div>
<div class="col"> <button id="map.btnalerts" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<button id="map.btnalerts" type="button" class="btn btn-primary w-100 h-100"> <i class="fa fa-exclamation-circle me-2"></i><span class="d-none d-sm-inline">Alerts</span>
<i class="fa fa-exclamation-circle me-1"></i><span class="d-none d-sm-inline">Alerts</span>
</button> </button>
</div>
<div class="col"> <button id="map.btngroups" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<button id="map.btngroups" type="button" class="btn btn-primary w-100 h-100"> <i class="fa fa-users me-2"></i><span class="d-none d-sm-inline">Groups</span>
<i class="fa fa-users me-1"></i><span class="d-none d-sm-inline">Groups</span>
</button> </button>
</div>
<div class="col"> <button id="map.btnlocate" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<button id="map.btnlocate" type="button" class="btn btn-primary w-100 h-100"> <i class="fa fa-location-arrow me-2"></i><span class="d-none d-sm-inline">Locate</span>
<i class="fa fa-location-arrow me-1"></i><span class="d-none d-sm-inline">Locate</span>
</button> </button>
</div>
<div class="col"> <button id="map.btnfilters" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<button id="map.btnfilters" type="button" class="btn btn-primary w-100 h-100"> <i class="fa fa-sliders-h me-2"></i><span class="d-none d-sm-inline">Filter</span>
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button> </button>
</div>
<div class="col"> <button id="map.btndisplay" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<button id="map.btndisplay" type="button" class="btn btn-primary w-100 h-100"> <i class="fa fa-sun me-2"></i><span class="d-none d-sm-inline">Display</span>
<i class="fa fa-sun me-1"></i><span class="d-none d-sm-inline">Display</span>
</button> </button>
</div>
</div>
</div>
</nav> </nav>
<!-- Map fills remaining space --> <div class="flex-grow-1 position-relative" style="min-height: 0;">
<div class="flex-grow-1" style="min-height:400px;"> <div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div>
<!-- TWebPanel (pnlMap) will render itself here -->
<div id="map_pnlmap" class="w-100 h-100"></div>
</div> </div>
</div> </div>
......
...@@ -23,18 +23,20 @@ type ...@@ -23,18 +23,20 @@ type
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
xdwcMap: TXDataWebClient; xdwcMap: TXDataWebClient;
tmrRefresh: TWebTimer; tmrRefresh: TWebTimer;
tmrZoomToBounds: TWebTimer;
procedure lfMapMapInitialized(Sender: TObject); procedure lfMapMapInitialized(Sender: TObject);
[async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
procedure lfMapCustomizeMarker(Sender: TObject; procedure lfMapCustomizeMarker(Sender: TObject;
var ACustomizeMarker: string); var ACustomizeMarker: string);
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrZoomToBoundsTimer(Sender: TObject);
private private
FUnitsLoaded: Boolean; FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean; FComplaintsLoaded: Boolean;
FZoomPending: Boolean;
[async] procedure LoadPointsAsync; [async] procedure LoadPointsAsync;
function CarIconForDistrict(const DistrictCode: string): string; function CarIconForDistrict(const DistrictCode: string): string;
procedure ShowUnitDetails(const unitId: string);
public public
end; end;
...@@ -60,7 +62,6 @@ begin ...@@ -60,7 +62,6 @@ begin
console.log('JS bridge showComplaintDetails called, id=', id); console.log('JS bridge showComplaintDetails called, id=', id);
try { try {
pas['View.Main'].FViewMain.ShowComplaintDetails(id); pas['View.Main'].FViewMain.ShowComplaintDetails(id);
console.log('TFViewMain.ShowComplaintDetails finished OK');
} catch (e) { } catch (e) {
console.log('Error in TFViewMain.ShowComplaintDetails', e); console.log('Error in TFViewMain.ShowComplaintDetails', e);
} }
...@@ -69,20 +70,15 @@ begin ...@@ -69,20 +70,15 @@ begin
window.showUnitDetails = function (id) { window.showUnitDetails = function (id) {
console.log('JS bridge showUnitDetails called, id=', id); console.log('JS bridge showUnitDetails called, id=', id);
try { try {
pas['View.Map'].FViewMap.ShowUnitDetails(id); pas['View.Main'].FViewMain.ShowUnitDetails(id);
} catch (e) { } catch (e) {
console.log('Error in TFViewMap.ShowUnitDetails', e); console.log('Error in TFViewMain.ShowUnitDetails', e);
} }
}; };
end; end;
{$ENDIF} {$ENDIF}
end; end;
procedure TFViewMap.ShowUnitDetails(const unitId: string);
begin
ShowMessage('Link to Unit Details form');
end;
[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var var
...@@ -137,7 +133,10 @@ begin ...@@ -137,7 +133,10 @@ begin
end; end;
if lfMap.Polygons.Count > 0 then if lfMap.Polygons.Count > 0 then
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray); begin
FZoomPending := True;
tmrZoomToBounds.Enabled := True;
end;
finally finally
lfMap.EndUpdate; lfMap.EndUpdate;
end; end;
...@@ -181,7 +180,6 @@ begin ...@@ -181,7 +180,6 @@ begin
ShowSpinner('spinner'); ShowSpinner('spinner');
FUnitsLoaded := False; FUnitsLoaded := False;
FComplaintsLoaded := False; FComplaintsLoaded := False;
// --- Units --------------------------------------------------------------- // --- Units ---------------------------------------------------------------
try try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', [])); resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
...@@ -376,7 +374,6 @@ begin ...@@ -376,7 +374,6 @@ begin
'var t = o.tooltipHtml || "";' + #13#10 + 'var t = o.tooltipHtml || "";' + #13#10 +
'var u = (o.icon && o.icon.options && o.icon.options.iconUrl) ? o.icon.options.iconUrl : null;' + #13#10 + 'var u = (o.icon && o.icon.options && o.icon.options.iconUrl) ? o.icon.options.iconUrl : null;' + #13#10 +
// clear any old tooltip/popup bindings
'try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 + 'try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
'try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 + 'try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
...@@ -431,28 +428,25 @@ end; ...@@ -431,28 +428,25 @@ end;
procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin begin
ACustomizeCSS := ACustomizeCSS :=
// popup container: rounded, shadow // --- Popup container----------------------------------------------------------
'.emi-tip .leaflet-popup-content-wrapper{' + '.emi-tip .leaflet-popup-content-wrapper{' +
'border-radius:8px;' + 'border-radius:8px;' +
'box-shadow:0 4px 14px rgba(0,0,0,.25);' + 'box-shadow:0 4px 14px rgba(0,0,0,.25);' +
'}' + #13#10 + '}' + #13#10 +
// --- Popup content------------------------------------------------------------
// popup content: no fixed width, max 260px on normal screens
'.emi-tip .leaflet-popup-content{' + '.emi-tip .leaflet-popup-content{' +
'margin:0;' + 'margin:0;' +
'padding:0;' + 'padding:0;' +
'width:auto;' + 'width:auto;' +
'max-width:260px;' + 'max-width:260px;' +
'}' + #13#10 + '}' + #13#10 +
// --- Media Query: on very small screens, lets it grow almost full width ------
// on very small screens, let it grow almost full width
'@media (max-width:480px){' + '@media (max-width:480px){' +
'.emi-tip .leaflet-popup-content{' + '.emi-tip .leaflet-popup-content{' +
'max-width:calc(100vw - 32px);' + 'max-width:calc(100vw - 32px);' +
'}' + '}' +
'}' + #13#10 + '}' + #13#10 +
// --- Table: compact, wraps text ----------------------------------------------
// table: compact, wraps text
'.emi-tip .emi-tip-table{display:table;border-collapse:collapse;table-layout:auto;width:auto;margin:.25rem 0;}'+#13#10+ '.emi-tip .emi-tip-table{display:table;border-collapse:collapse;table-layout:auto;width:auto;margin:.25rem 0;}'+#13#10+
'.emi-tip .emi-tip-table thead{display:table-header-group;}'+#13#10+ '.emi-tip .emi-tip-table thead{display:table-header-group;}'+#13#10+
'.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+ '.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+
...@@ -465,13 +459,27 @@ begin ...@@ -465,13 +459,27 @@ begin
'font-size:11px;' + 'font-size:11px;' +
'vertical-align:middle;' + 'vertical-align:middle;' +
'}' + #13#10 + '}' + #13#10 +
// --- Marker Badge ------------------------------------------------------------
// marker badge
'.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+ '.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
'.emi-marker-img{display:block;}'+#13#10+ '.emi-marker-img{display:block;}'+#13#10+
'.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}'; '.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}';
end; end;
procedure TFViewMap.tmrZoomToBoundsTimer(Sender: TObject);
begin
tmrZoomToBounds.Enabled := False;
if not FZoomPending then
Exit;
FZoomPending := False;
if lfMap.Polygons.Count = 0 then
Exit;
// Note: Delaying avoids Leaflet moveend firing while the internal map is still null.
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
end;
end. end.
object FViewUnitDetails: TFViewUnitDetails
Width = 640
Height = 480
end
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>TMS Web Project</title>
<style>
</style>
</head>
<body>
</body>
</html>
\ No newline at end of file
unit View.UnitDetails;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs;
type
TFViewUnitDetails = class(TWebForm)
private
FUnitId: string;
procedure InitializeForm;
public
class function CreateForm(AElementID, UnitId: string): TWebForm;
end;
var
FViewUnitDetails: TFViewUnitDetails;
implementation
{$R *.dfm}
class function TFViewUnitDetails.CreateForm(AElementID, UnitId: string): TWebForm;
procedure AfterCreate(AForm: TObject);
begin
with TFViewUnitDetails(AForm) do
begin
FUnitId := UnitId;
InitializeForm; // kick off loading / UI binding here
end;
end;
begin
Application.CreateForm(TFViewUnitDetails, AElementID, Result, @AfterCreate);
end;
procedure TFViewUnitDetails.InitializeForm;
begin
// TODO:
// - call your XData endpoint with FUnitId
// - bind fields / populate controls
// - handle spinner / errors as you do elsewhere
end;
end.
\ No newline at end of file
/* --- TMS WEB Core Specific Fixes --- */
/* Removes the default border from the main Form wrapper */
span.card {
border: none;
}
/* --- Login Screen Styling --- */
.login-card { .login-card {
display: inline-block; display: inline-block; /* Or use d-flex on the parent to center it */
width: 300px; /* Adjust width as needed */ width: 100%;
max-width: 350px; /* Better than fixed 300px */
padding: 0; padding: 0;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff; background-color: #fff;
} }
.mr-2 { /* Optional: Custom navbar look for the login screen */
margin-right: 0.5rem;
}
.table tbody tr:hover {
background-color: #d1e7fd; /* Light blue color for hover effect */
cursor: pointer;
}
.form-input{
display: table;
}
.form-cells{
display: table-cell
}
.login-navbar { .login-navbar {
max-width: 1200px; /* Set the max-width to match a medium screen */ max-width: 1200px;
margin: auto; margin: auto;
border-bottom-left-radius: 10px; /* Round the bottom left corner */ border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px; /* Round the bottom right corner */ border-bottom-right-radius: 10px;
border: 1px solid #d3d3d3; border: 1px solid #d3d3d3;
} }
.navbar-toggler { /* --- Table Customization --- */
display: none; /* Bootstrap has .table-hover, but this sets your specific blue color */
} .table tbody tr:hover {
background-color: #d1e7fd;
.dropdown-menu a { cursor: pointer;
display: flex; /* Use flexbox for alignment */
align-items: center; /* Vertically center the content */
width: 100%; /* Ensure they take up the full width */
padding: 0.5rem 1rem; /* Add padding to make them clickable */
color: #000; /* Adjust the text color if necessary */
text-decoration: none; /* Remove underlines */
}
.dropdown-menu a:hover {
background-color: #204d74;
color: #fff;
}
.dropdown-menu a span {
flex-grow: 1; /* Make the span take up the remaining space */
} }
/* Style for the selected number */ /* --- Pagination Theme Overrides --- */
.selected-number .page-link { /* These override Bootstrap's blue to your specific darker blue (#204d74) */
background-color: #204d74;
color: #fff !important;
}
/* Style for the unselected numbers and text (previous/next) */ .pagination .page-link {
.pagination .page-item a, color: #204d74; /* Text color for standard links */
.pagination .page-item span {
color: #204d74;
} }
.pagination .page-item.active .page-link, /* Active State (Selected Page) */
.pagination .page-item.active .page-link:hover, .pagination .page-item.active .page-link {
.pagination .page-item.active .page-link:focus {
background-color: #204d74; background-color: #204d74;
border-color: #204d74; border-color: #204d74;
color: #fff !important; color: #fff !important;
} }
/* This is needed to get rid of the line that was appearing. */ /* Hover State */
span.card { .pagination .page-item:not(.active) .page-link:hover {
border: none; background-color: #e9ecef; /* Standard Bootstrap light grey hover */
color: #16344a; /* Darker text on hover */
} }
/* --- Utilities --- */
.modal-backdrop { .list-section-header:empty {
z-index: 1040 !important; display: none;
}
.modal {
z-index: 1055 !important;
} }
#map_pnlmap > span {
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
position: absolute !important;
}
.list-section-header:empty { display: none; } html, body {
height: 100%;
overflow: hidden;
}
...@@ -21,7 +21,8 @@ uses ...@@ -21,7 +21,8 @@ uses
View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html}, View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html},
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html}, View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html}; View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html};
{$R *.res} {$R *.res}
......
...@@ -181,6 +181,11 @@ ...@@ -181,6 +181,11 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="View.UnitDetails.pas">
<Form>FViewUnitDetails</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<None Include="index.html"/> <None Include="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
......
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