Check if we have oauth information, only prompt if its missing
Also include a rough dashboard of information that we'll pull from Fieldseeker
This commit is contained in:
parent
fa89e0719f
commit
72cbe2de5e
10 changed files with 567 additions and 100 deletions
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/arcgis-go"
|
"github.com/Gleipnir-Technology/arcgis-go"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/models"
|
"github.com/Gleipnir-Technology/nidus-sync/models"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/sql"
|
||||||
"github.com/aarondl/opt/omit"
|
"github.com/aarondl/opt/omit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -155,6 +156,13 @@ func handleOauthAccessCode(ctx context.Context, user *models.User, code string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasFieldseekerConnection(ctx context.Context, user *models.User) (bool, error) {
|
||||||
|
result, err := sql.OauthTokenByUserId(user.ID).All(ctx, PGInstance.BobDB)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(result) > 0, nil
|
||||||
|
}
|
||||||
func redirectURL() string {
|
func redirectURL() string {
|
||||||
return BaseURL + "/arcgis/oauth/callback"
|
return BaseURL + "/arcgis/oauth/callback"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
endpoint.go
11
endpoint.go
|
|
@ -167,7 +167,16 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
errorCode := r.URL.Query().Get("error")
|
errorCode := r.URL.Query().Get("error")
|
||||||
err = htmlSignin(w, errorCode)
|
err = htmlSignin(w, errorCode)
|
||||||
} else {
|
} else {
|
||||||
err = htmlDashboard(w, user)
|
has, err := hasFieldseekerConnection(r.Context(), user)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
err = htmlDashboard(w, user)
|
||||||
|
} else {
|
||||||
|
err = htmlOauthPrompt(w, user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
|
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
|
||||||
|
|
|
||||||
12
html.go
12
html.go
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dashboard = newBuiltTemplate("dashboard", "authenticated")
|
dashboard = newBuiltTemplate("dashboard", "authenticated")
|
||||||
|
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
|
||||||
report = newBuiltTemplate("report", "base")
|
report = newBuiltTemplate("report", "base")
|
||||||
reportConfirmation = newBuiltTemplate("report-confirmation", "base")
|
reportConfirmation = newBuiltTemplate("report-confirmation", "base")
|
||||||
reportContribute = newBuiltTemplate("report-contribute", "base")
|
reportContribute = newBuiltTemplate("report-contribute", "base")
|
||||||
|
|
@ -99,6 +100,17 @@ func htmlDashboard(w io.Writer, user *models.User) error {
|
||||||
return dashboard.ExecuteTemplate(w, data)
|
return dashboard.ExecuteTemplate(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func htmlOauthPrompt(w io.Writer, user *models.User) error {
|
||||||
|
data := ContentDashboard{
|
||||||
|
User: User{
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
Initials: extractInitials(user.DisplayName),
|
||||||
|
Username: user.Username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return oauthPrompt.ExecuteTemplate(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
func htmlReport(w io.Writer) error {
|
func htmlReport(w io.Writer) error {
|
||||||
url := BaseURL + "/report/t78fd3"
|
url := BaseURL + "/report/t78fd3"
|
||||||
data := ContentReportDiagnostic{
|
data := ContentReportDiagnostic{
|
||||||
|
|
|
||||||
102
sql/oauth_by_user_id.bob.go
Normal file
102
sql/oauth_by_user_id.bob.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
|
||||||
|
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||||
|
|
||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stephenafamo/bob"
|
||||||
|
"github.com/stephenafamo/bob/dialect/psql"
|
||||||
|
"github.com/stephenafamo/bob/dialect/psql/dialect"
|
||||||
|
"github.com/stephenafamo/bob/orm"
|
||||||
|
"github.com/stephenafamo/scan"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed oauth_by_user_id.bob.sql
|
||||||
|
var formattedQueries_oauth_by_user_id string
|
||||||
|
|
||||||
|
var oauthTokenByUserIdSQL = formattedQueries_oauth_by_user_id[156:440]
|
||||||
|
|
||||||
|
type OauthTokenByUserIdQuery = orm.ModQuery[*dialect.SelectQuery, oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer]
|
||||||
|
|
||||||
|
func OauthTokenByUserId(UserID int32) *OauthTokenByUserIdQuery {
|
||||||
|
var expressionTypArgs oauthTokenByUserId
|
||||||
|
|
||||||
|
expressionTypArgs.UserID = psql.Arg(UserID)
|
||||||
|
|
||||||
|
return &OauthTokenByUserIdQuery{
|
||||||
|
Query: orm.Query[oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer]{
|
||||||
|
ExecQuery: orm.ExecQuery[oauthTokenByUserId]{
|
||||||
|
BaseQuery: bob.BaseQuery[oauthTokenByUserId]{
|
||||||
|
Expression: expressionTypArgs,
|
||||||
|
Dialect: dialect.Dialect,
|
||||||
|
QueryType: bob.QueryTypeSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scanner: func(context.Context, []string) (func(*scan.Row) (any, error), func(any) (OauthTokenByUserIdRow, error)) {
|
||||||
|
return func(row *scan.Row) (any, error) {
|
||||||
|
var t OauthTokenByUserIdRow
|
||||||
|
row.ScheduleScanByIndex(0, &t.ID)
|
||||||
|
row.ScheduleScanByIndex(1, &t.AccessToken)
|
||||||
|
row.ScheduleScanByIndex(2, &t.Expires)
|
||||||
|
row.ScheduleScanByIndex(3, &t.RefreshToken)
|
||||||
|
row.ScheduleScanByIndex(4, &t.Username)
|
||||||
|
row.ScheduleScanByIndex(5, &t.UserID)
|
||||||
|
return &t, nil
|
||||||
|
}, func(v any) (OauthTokenByUserIdRow, error) {
|
||||||
|
return *(v.(*OauthTokenByUserIdRow)), nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) {
|
||||||
|
q.AppendSelect(expressionTypArgs.subExpr(7, 247))
|
||||||
|
q.SetTable(expressionTypArgs.subExpr(253, 264))
|
||||||
|
q.AppendWhere(expressionTypArgs.subExpr(272, 284))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OauthTokenByUserIdRow = struct {
|
||||||
|
ID int32 `db:"id"`
|
||||||
|
AccessToken string `db:"access_token"`
|
||||||
|
Expires time.Time `db:"expires"`
|
||||||
|
RefreshToken string `db:"refresh_token"`
|
||||||
|
Username string `db:"username"`
|
||||||
|
UserID int32 `db:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauthTokenByUserIdTransformer = bob.SliceTransformer[OauthTokenByUserIdRow, []OauthTokenByUserIdRow]
|
||||||
|
|
||||||
|
type oauthTokenByUserId struct {
|
||||||
|
UserID bob.Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oauthTokenByUserId) args() iter.Seq[orm.ArgWithPosition] {
|
||||||
|
return func(yield func(arg orm.ArgWithPosition) bool) {
|
||||||
|
if !yield(orm.ArgWithPosition{
|
||||||
|
Name: "userID",
|
||||||
|
Start: 282,
|
||||||
|
Stop: 284,
|
||||||
|
Expression: o.UserID,
|
||||||
|
}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oauthTokenByUserId) raw(from, to int) string {
|
||||||
|
return oauthTokenByUserIdSQL[from:to]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oauthTokenByUserId) subExpr(from, to int) bob.Expression {
|
||||||
|
return orm.ArgsToExpression(oauthTokenByUserIdSQL, from, to, o.args())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oauthTokenByUserId) WriteSQL(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
|
||||||
|
return o.subExpr(0, len(oauthTokenByUserIdSQL)).WriteSQL(ctx, w, d, start)
|
||||||
|
}
|
||||||
6
sql/oauth_by_user_id.bob.sql
Normal file
6
sql/oauth_by_user_id.bob.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Code generated by BobGen psql v0.41.1. DO NOT EDIT.
|
||||||
|
-- This file is meant to be re-generated in place and/or deleted at any time.
|
||||||
|
|
||||||
|
-- OauthTokenByUserId
|
||||||
|
SELECT "oauth_token"."id" AS "id", "oauth_token"."access_token" AS "access_token", "oauth_token"."expires" AS "expires", "oauth_token"."refresh_token" AS "refresh_token", "oauth_token"."username" AS "username", "oauth_token"."user_id" AS "user_id" FROM oauth_token WHERE
|
||||||
|
user_id = $1;
|
||||||
115
sql/oauth_by_user_id.bob_test.go
Normal file
115
sql/oauth_by_user_id.bob_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
|
||||||
|
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||||
|
|
||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stephenafamo/bob"
|
||||||
|
"github.com/stephenafamo/bob/dialect/psql"
|
||||||
|
testutils "github.com/stephenafamo/bob/test/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOauthTokenByUserId(t *testing.T) {
|
||||||
|
t.Run("Base", func(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
query := OauthTokenByUserId(random_int32(nil))
|
||||||
|
|
||||||
|
if _, err := query.WriteQuery(t.Context(), &sb, 1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(oauthTokenByUserIdSQL, sb.String()); diff != "" {
|
||||||
|
t.Fatalf("unexpected result (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Mod", func(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
query := OauthTokenByUserId(random_int32(nil))
|
||||||
|
|
||||||
|
if _, err := psql.Select(query).WriteQuery(t.Context(), &sb, 1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryDiff, err := testutils.QueryDiff(oauthTokenByUserIdSQL, sb.String(), formatQuery)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if queryDiff != "" {
|
||||||
|
fmt.Println(sb.String())
|
||||||
|
t.Fatalf("unexpected result (-got +want):\n%s", queryDiff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Scanning", func(t *testing.T) {
|
||||||
|
if testDB == nil {
|
||||||
|
t.Skip("skipping test, no DSN provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxTx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tx, err := testDB.Begin(ctxTx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error starting transaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := tx.Rollback(ctxTx); err != nil {
|
||||||
|
t.Fatalf("Error rolling back transaction: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
query, args, err := bob.Build(ctxTx, psql.Select(OauthTokenByUserId(random_int32(nil))))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctxTx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) != 6 {
|
||||||
|
t.Fatalf("expected %d columns, got %d", 6, len(columns))
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[0] != "id" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 0, "id", columns[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[1] != "access_token" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 1, "access_token", columns[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[2] != "expires" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 2, "expires", columns[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[3] != "refresh_token" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 3, "refresh_token", columns[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[4] != "username" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 4, "username", columns[4])
|
||||||
|
}
|
||||||
|
|
||||||
|
if columns[5] != "user_id" {
|
||||||
|
t.Fatalf("expected column %d to be %s, got %s", 5, "user_id", columns[5])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
3
sql/oauth_by_user_id.sql
Normal file
3
sql/oauth_by_user_id.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- OauthTokenByUserId
|
||||||
|
SELECT * FROM oauth_token WHERE
|
||||||
|
user_id = $1;
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{template "title" .}} - Nidus Sync</title>
|
<title>{{template "title" .}} - Nidus Sync</title>
|
||||||
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet">
|
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
{{template "style" .}}
|
{{template "style" .}}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,105 +2,210 @@
|
||||||
|
|
||||||
{{define "title"}}Dash{{end}}
|
{{define "title"}}Dash{{end}}
|
||||||
{{define "style"}}
|
{{define "style"}}
|
||||||
.connect-container {
|
body {
|
||||||
max-width: 800px;
|
background-color: #f8f9fa;
|
||||||
margin: 0 auto;
|
}
|
||||||
}
|
.dashboard-container {
|
||||||
.connect-box {
|
padding: 20px 0;
|
||||||
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
}
|
||||||
border-radius: 10px;
|
.stats-card {
|
||||||
padding: 40px;
|
border-radius: 10px;
|
||||||
background-color: #fff;
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||||
}
|
transition: transform 0.2s;
|
||||||
.connect-header {
|
height: 100%;
|
||||||
margin-bottom: 25px;
|
}
|
||||||
text-align: center;
|
.stats-card:hover {
|
||||||
}
|
transform: translateY(-5px);
|
||||||
.logo-area {
|
}
|
||||||
text-align: center;
|
.map-container {
|
||||||
margin-bottom: 30px;
|
background-color: #e9ecef;
|
||||||
}
|
border-radius: 10px;
|
||||||
.logo-placeholder {
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||||
width: 120px;
|
height: 500px;
|
||||||
height: 60px;
|
display: flex;
|
||||||
background-color: #e9ecef;
|
align-items: center;
|
||||||
margin: 0 auto;
|
justify-content: center;
|
||||||
display: flex;
|
margin-top: 20px;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
.section-title {
|
||||||
border-radius: 6px;
|
margin: 30px 0 20px;
|
||||||
}
|
padding-bottom: 10px;
|
||||||
.steps-container {
|
border-bottom: 1px solid #dee2e6;
|
||||||
margin: 30px 0;
|
}
|
||||||
}
|
.last-refreshed {
|
||||||
.step {
|
color: #6c757d;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
padding: 15px;
|
.logo-placeholder {
|
||||||
border-left: 3px solid #0d6efd;
|
width: 100px;
|
||||||
background-color: #f8f9fa;
|
height: 40px;
|
||||||
}
|
background-color: #e9ecef;
|
||||||
.connect-btn {
|
display: flex;
|
||||||
margin-top: 30px;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.metric-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
|
<div class="container dashboard-container">
|
||||||
<div class="connect-container">
|
<!-- Dashboard Header -->
|
||||||
<!-- Logo Area -->
|
<div class="row mb-4">
|
||||||
<div class="logo-area">
|
<div class="col-md-6">
|
||||||
<div class="logo-placeholder">
|
<h1>Mosquito District Dashboard</h1>
|
||||||
<span class="text-muted">Your Logo</span>
|
<p class="text-muted">Overview of mosquito control activities in your district</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end">
|
||||||
|
<p class="last-refreshed mb-0">
|
||||||
<div class="connect-box">
|
<i class="fas fa-sync-alt me-2"></i>Last updated: <span id="last-refreshed-time">3 hours ago</span>
|
||||||
<div class="connect-header">
|
<button class="btn btn-sm btn-outline-primary ms-3">Refresh Data</button>
|
||||||
<h1>Connect Your ArcGIS Account</h1>
|
</p>
|
||||||
<p class="text-muted">Link your data to get started</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connect-content">
|
<!-- Key Metrics -->
|
||||||
<p>To provide you with the best experience, we need to connect to your ArcGIS account. This allows us to securely access and visualize your spatial data within our platform.</p>
|
<div class="row g-4">
|
||||||
|
<!-- Last Refreshed -->
|
||||||
<div class="steps-container">
|
<div class="col-md-3">
|
||||||
<h4>What to expect:</h4>
|
<div class="card stats-card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
<div class="step">
|
<div class="metric-icon bg-info bg-opacity-10 text-info">
|
||||||
<h5>1. Secure Authentication</h5>
|
<i class="fas fa-clock"></i>
|
||||||
<p>When you click the "Connect to ArcGIS" button below, you'll be redirected to the official ArcGIS login page. This connection is secure and uses OAuth 2.0 protocol.</p>
|
</div>
|
||||||
</div>
|
<h5 class="card-title">Last Data Refresh</h5>
|
||||||
|
<p class="metric-value">3h</p>
|
||||||
<div class="step">
|
<p class="card-text text-muted">Last sync: 12:45 PM</p>
|
||||||
<h5>2. Grant Permissions</h5>
|
</div>
|
||||||
<p>After logging in with your ArcGIS credentials, you'll be asked to approve permissions for our application to access your data. We only request access to what's needed for the platform to function.</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<!-- Service Requests -->
|
||||||
<h5>3. Return to Platform</h5>
|
<div class="col-md-3">
|
||||||
<p>Once authentication is complete, you'll be automatically redirected back to our platform where your data will be available to work with.</p>
|
<div class="card stats-card h-100">
|
||||||
</div>
|
<div class="card-body text-center">
|
||||||
</div>
|
<div class="metric-icon bg-warning bg-opacity-10 text-warning">
|
||||||
|
<i class="fas fa-ticket-alt"></i>
|
||||||
<div class="alert alert-info">
|
</div>
|
||||||
<strong>Note:</strong> You'll need an active ArcGIS Online account or ArcGIS Enterprise account to proceed. If you don't have one, you can <a href="https://www.arcgis.com/home/signin.html" target="_blank">create an ArcGIS account here</a>.
|
<h5 class="card-title">Service Requests</h5>
|
||||||
</div>
|
<p class="metric-value">48</p>
|
||||||
|
<p class="card-text text-muted">
|
||||||
<p>By connecting your ArcGIS account, you'll be able to:</p>
|
<span class="text-success">
|
||||||
<ul>
|
<i class="fas fa-arrow-up"></i> 12%
|
||||||
<li>Access and visualize your spatial data</li>
|
</span> since last week
|
||||||
<li>Perform advanced analysis using our integrated tools</li>
|
</p>
|
||||||
<li>Share results with team members securely</li>
|
</div>
|
||||||
<li>Keep your data synchronized across platforms</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
<div class="text-center connect-btn">
|
<!-- Mosquito Sources -->
|
||||||
<a href="/arcgis/oauth/begin" class="btn btn-primary btn-lg">
|
<div class="col-md-3">
|
||||||
Connect to ArcGIS
|
<div class="card stats-card h-100">
|
||||||
</a>
|
<div class="card-body text-center">
|
||||||
<p class="mt-2 text-muted"><small>You can disconnect your account at any time in settings</small></p>
|
<div class="metric-icon bg-danger bg-opacity-10 text-danger">
|
||||||
</div>
|
<i class="fas fa-bug"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h5 class="card-title">Mosquito Sources</h5>
|
||||||
</div>
|
<p class="metric-value">124</p>
|
||||||
</div>
|
<p class="card-text text-muted">
|
||||||
|
<span class="text-danger">
|
||||||
|
<i class="fas fa-arrow-up"></i> 8%
|
||||||
|
</span> since last month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inspections -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="metric-icon bg-success bg-opacity-10 text-success">
|
||||||
|
<i class="fas fa-clipboard-check"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title">Inspections</h5>
|
||||||
|
<p class="metric-value">76</p>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="fas fa-arrow-up"></i> 15%
|
||||||
|
</span> since last week
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Section -->
|
||||||
|
<h3 class="section-title">Mosquito Activity Heatmap</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="map-container" id="mosquito-heatmap">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fas fa-map-marked-alt fa-4x text-muted mb-3"></i>
|
||||||
|
<h4>Mosquito Activity Heatmap</h4>
|
||||||
|
<p class="text-muted">Map visualization will be displayed here</p>
|
||||||
|
<small>Showing activity data for the Central District area</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity Section -->
|
||||||
|
<h3 class="section-title">Recent Activity</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Aug 24, 2023</td>
|
||||||
|
<td>Inspection</td>
|
||||||
|
<td>River Park Area</td>
|
||||||
|
<td><span class="badge bg-success">Completed</span></td>
|
||||||
|
<td><a href="#" class="btn btn-sm btn-outline-primary">View</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Aug 23, 2023</td>
|
||||||
|
<td>Service Request</td>
|
||||||
|
<td>Westside Community</td>
|
||||||
|
<td><span class="badge bg-warning">Pending</span></td>
|
||||||
|
<td><a href="#" class="btn btn-sm btn-outline-primary">View</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Aug 22, 2023</td>
|
||||||
|
<td>Source Treatment</td>
|
||||||
|
<td>Lakeside Avenue</td>
|
||||||
|
<td><span class="badge bg-info">In Progress</span></td>
|
||||||
|
<td><a href="#" class="btn btn-sm btn-outline-primary">View</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
106
templates/oauth-prompt.html
Normal file
106
templates/oauth-prompt.html
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
{{template "authenticated.html" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Dash{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
.connect-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.connect-box {
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.connect-header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.logo-area {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 120px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.steps-container {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-left: 3px solid #0d6efd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.connect-btn {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
|
||||||
|
<div class="connect-container">
|
||||||
|
<!-- Logo Area -->
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-placeholder">
|
||||||
|
<span class="text-muted">Your Logo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connect-box">
|
||||||
|
<div class="connect-header">
|
||||||
|
<h1>Connect Your ArcGIS Account</h1>
|
||||||
|
<p class="text-muted">Link your data to get started</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connect-content">
|
||||||
|
<p>To provide you with the best experience, we need to connect to your ArcGIS account. This allows us to securely access and visualize your spatial data within our platform.</p>
|
||||||
|
|
||||||
|
<div class="steps-container">
|
||||||
|
<h4>What to expect:</h4>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h5>1. Secure Authentication</h5>
|
||||||
|
<p>When you click the "Connect to ArcGIS" button below, you'll be redirected to the official ArcGIS login page. This connection is secure and uses OAuth 2.0 protocol.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h5>2. Grant Permissions</h5>
|
||||||
|
<p>After logging in with your ArcGIS credentials, you'll be asked to approve permissions for our application to access your data. We only request access to what's needed for the platform to function.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<h5>3. Return to Platform</h5>
|
||||||
|
<p>Once authentication is complete, you'll be automatically redirected back to our platform where your data will be available to work with.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Note:</strong> You'll need an active ArcGIS Online account or ArcGIS Enterprise account to proceed. If you don't have one, you can <a href="https://www.arcgis.com/home/signin.html" target="_blank">create an ArcGIS account here</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>By connecting your ArcGIS account, you'll be able to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Access and visualize your spatial data</li>
|
||||||
|
<li>Perform advanced analysis using our integrated tools</li>
|
||||||
|
<li>Share results with team members securely</li>
|
||||||
|
<li>Keep your data synchronized across platforms</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="text-center connect-btn">
|
||||||
|
<a href="/arcgis/oauth/begin" class="btn btn-primary btn-lg">
|
||||||
|
Connect to ArcGIS
|
||||||
|
</a>
|
||||||
|
<p class="mt-2 text-muted"><small>You can disconnect your account at any time in settings</small></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue