Pretty all the things I missed
My laptop didn't have lefthook running. Oops.
This commit is contained in:
parent
f60bde7fd9
commit
4bbfbdb9e6
30 changed files with 490 additions and 487 deletions
|
|
@ -119,7 +119,7 @@ func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFu
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
path, e := f(ctx, r, *req)
|
path, e := f(ctx, r, *req)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, path, http.StatusFound)
|
http.Redirect(w, r, path, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +128,7 @@ func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFu
|
||||||
type postMultipartResponse struct {
|
type postMultipartResponse struct {
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file.Upload], collection file.Collection) http.Handler {
|
func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file.Upload], collection file.Collection) http.Handler {
|
||||||
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
|
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
|
||||||
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
|
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
|
||||||
|
|
@ -142,11 +143,11 @@ func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
err = decoder.Decode(&content, r.PostForm)
|
err = decoder.Decode(&content, r.PostForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err)
|
respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
path, e := f(ctx, r, u, uploads)
|
path, e := f(ctx, r, u, uploads)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type reqSignin struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) {
|
func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) {
|
||||||
if req.Password == "" {
|
if req.Password == "" {
|
||||||
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password")
|
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password")
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ import (
|
||||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type reqSignup struct {
|
type reqSignup struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Terms bool `json:"terms"`
|
Terms bool `json:"terms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, *nhttp.ErrorWithStatus) {
|
func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, *nhttp.ErrorWithStatus) {
|
||||||
|
|
||||||
log.Info().Str("username", signup.Username).Str("name", signup.Name).Str("password", strings.Repeat("*", len(signup.Password))).Msg("Signup")
|
log.Info().Str("username", signup.Username).Str("name", signup.Name).Str("password", strings.Repeat("*", len(signup.Password))).Msg("Signup")
|
||||||
|
|
@ -33,4 +35,3 @@ func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string,
|
||||||
|
|
||||||
return "/", nil
|
return "/", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceRequestSummary struct {
|
type ServiceRequestSummary struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Location string
|
Location string
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ func SignalList(ctx context.Context, user User, limit int) ([]*Signal, error) {
|
||||||
row.Pool = p
|
row.Pool = p
|
||||||
row.Report = nil
|
row.Report = nil
|
||||||
} else if row.Report.ID != 0 {
|
} else if row.Report.ID != 0 {
|
||||||
report, ok := report_map[row.Report.ID]
|
report, ok := report_map[row.Report.ID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to get report %d for %d", row.Report.ID, row.ID)
|
return nil, fmt.Errorf("failed to get report %d for %d", row.Report.ID, row.ID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func newUser(ctx context.Context, org Organization, user *models.User) User {
|
||||||
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
||||||
o_setter := models.OrganizationSetter{
|
o_setter := models.OrganizationSetter{
|
||||||
IsCatchall: omit.From(false),
|
IsCatchall: omit.From(false),
|
||||||
Name: omit.From(fmt.Sprintf("%s's organization", username)),
|
Name: omit.From(fmt.Sprintf("%s's organization", username)),
|
||||||
}
|
}
|
||||||
o, err := models.Organizations.Insert(&o_setter).One(ctx, db.PGInstance.BobDB)
|
o, err := models.Organizations.Insert(&o_setter).One(ctx, db.PGInstance.BobDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
22
sync/cell.go
22
sync/cell.go
|
|
@ -38,15 +38,15 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (*
|
||||||
return nil, nhttp.NewError("Failed to get inspections by cell: %w", err)
|
return nil, nhttp.NewError("Failed to get inspections by cell: %w", err)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
center, err := h3.Cell(c).LatLng()
|
center, err := h3.Cell(c).LatLng()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nhttp.NewError("Failed to get center: %w", err)
|
return nil, nhttp.NewError("Failed to get center: %w", err)
|
||||||
}
|
}
|
||||||
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
|
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nhttp.NewError("Failed to get boundaries: %w", err)
|
return nil, nhttp.NewError("Failed to get boundaries: %w", err)
|
||||||
}
|
}
|
||||||
resolution := h3.Cell(c).Resolution()
|
resolution := h3.Cell(c).Resolution()
|
||||||
*/
|
*/
|
||||||
sources, err := platform.BreedingSourcesByCell(ctx, user.Organization, h3.Cell(c))
|
sources, err := platform.BreedingSourcesByCell(ctx, user.Organization, h3.Cell(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -65,7 +65,7 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (*
|
||||||
BreedingSources: sources,
|
BreedingSources: sources,
|
||||||
CellBoundary: boundary,
|
CellBoundary: boundary,
|
||||||
Inspections: inspections,
|
Inspections: inspections,
|
||||||
Traps: traps,
|
Traps: traps,
|
||||||
Treatments: treatments,
|
Treatments: treatments,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
sync/dash.go
14
sync/dash.go
|
|
@ -21,8 +21,8 @@ type contentSource struct {
|
||||||
User platform.User
|
User platform.User
|
||||||
}
|
}
|
||||||
type contentTrap struct {
|
type contentTrap struct {
|
||||||
Trap platform.Trap
|
Trap platform.Trap
|
||||||
User platform.User
|
User platform.User
|
||||||
}
|
}
|
||||||
type contentLayoutTest struct {
|
type contentLayoutTest struct {
|
||||||
User platform.User
|
User platform.User
|
||||||
|
|
@ -71,7 +71,7 @@ func getSource(ctx context.Context, r *http.Request, user platform.User) (*html.
|
||||||
}
|
}
|
||||||
treatment_models := platform.ModelTreatment(treatments)
|
treatment_models := platform.ModelTreatment(treatments)
|
||||||
data := contentSource{
|
data := contentSource{
|
||||||
Inspections: inspections,
|
Inspections: inspections,
|
||||||
Source: s,
|
Source: s,
|
||||||
Traps: traps,
|
Traps: traps,
|
||||||
Treatments: treatments,
|
Treatments: treatments,
|
||||||
|
|
@ -99,10 +99,10 @@ func getTrap(ctx context.Context, r *http.Request, user platform.User) (*html.Re
|
||||||
return nil, nhttp.NewError("Failed to get trap: %w", err)
|
return nil, nhttp.NewError("Failed to get trap: %w", err)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
latlng, err := t.H3Cell.LatLng()
|
latlng, err := t.H3Cell.LatLng()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nhttp.NewError("Failed to get latlng: %w", err)
|
return nil, nhttp.NewError("Failed to get latlng: %w", err)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
data := contentTrap{
|
data := contentTrap{
|
||||||
Trap: *t,
|
Trap: *t,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import ()
|
||||||
)
|
|
||||||
|
|
|
||||||
26
ts/client.ts
26
ts/client.ts
|
|
@ -1,24 +1,24 @@
|
||||||
// src/api/axios.js or similar
|
// src/api/axios.js or similar
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import router from '@/router';
|
import router from "@/router";
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: "/api",
|
||||||
withCredentials: true
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response interceptor to catch auth failures
|
// Response interceptor to catch auth failures
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
// Session expired or not authenticated
|
// Session expired or not authenticated
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
apiClient.isAuthenticated = () => {
|
apiClient.isAuthenticated = () => {
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
|
|
|
||||||
|
|
@ -1,209 +1,220 @@
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.upload-widget {
|
.upload-widget {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-area {
|
.upload-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 2px dashed #cbd5e0;
|
border: 2px dashed #cbd5e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: #f7fafc;
|
background-color: #f7fafc;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-area:hover {
|
.upload-area:hover {
|
||||||
border-color: #4299e1;
|
border-color: #4299e1;
|
||||||
background-color: #ebf8ff;
|
background-color: #ebf8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input {
|
.file-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input:disabled {
|
.file-input:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-info {
|
.upload-info {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder svg {
|
.placeholder svg {
|
||||||
margin: 0 auto 1rem;
|
margin: 0 auto 1rem;
|
||||||
color: #a0aec0;
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder p {
|
.placeholder p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-selected {
|
.file-selected {
|
||||||
color: #2d3748;
|
color: #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name {
|
.file-name {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-size {
|
.file-size {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: #718096;
|
color: #718096;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-button {
|
.upload-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background-color: #4299e1;
|
background-color: #4299e1;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-button:hover {
|
.upload-button:hover {
|
||||||
background-color: #3182ce;
|
background-color: #3182ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-button:disabled {
|
.upload-button:disabled {
|
||||||
background-color: #cbd5e0;
|
background-color: #cbd5e0;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-status {
|
.upload-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background-color: #ebf8ff;
|
background-color: #ebf8ff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #2c5282;
|
color: #2c5282;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: 3px solid #bee3f8;
|
border: 3px solid #bee3f8;
|
||||||
border-top-color: #4299e1;
|
border-top-color: #4299e1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message {
|
.success-message {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background-color: #c6f6d5;
|
background-color: #c6f6d5;
|
||||||
color: #22543d;
|
color: #22543d;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background-color: #fed7d7;
|
background-color: #fed7d7;
|
||||||
color: #742a2a;
|
color: #742a2a;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div class="upload-widget">
|
<div class="upload-widget">
|
||||||
<div class="upload-area">
|
<div class="upload-area">
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
@change="handleFileSelect"
|
@change="handleFileSelect"
|
||||||
:disabled="isUploading"
|
:disabled="isUploading"
|
||||||
class="file-input"
|
class="file-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="upload-info">
|
|
||||||
<div v-if="!selectedFile" class="placeholder">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
||||||
<polyline points="17 8 12 3 7 8"></polyline>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
||||||
</svg>
|
|
||||||
<p>Select a CSV file to upload</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="file-selected">
|
|
||||||
<p class="file-name">{{ selectedFile.name }}</p>
|
|
||||||
<p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<div class="upload-info">
|
||||||
v-if="selectedFile && !isUploading"
|
<div v-if="!selectedFile" class="placeholder">
|
||||||
@click="uploadFile"
|
<svg
|
||||||
class="upload-button"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
:disabled="isUploading"
|
width="48"
|
||||||
>
|
height="48"
|
||||||
Upload File
|
viewBox="0 0 24 24"
|
||||||
</button>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<p>Select a CSV file to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="isUploading" class="upload-status">
|
<div v-else class="file-selected">
|
||||||
<div class="spinner"></div>
|
<p class="file-name">{{ selectedFile.name }}</p>
|
||||||
<p>Uploading...</p>
|
<p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="uploadSuccess" class="success-message">
|
<button
|
||||||
✓ File uploaded successfully!
|
v-if="selectedFile && !isUploading"
|
||||||
</div>
|
@click="uploadFile"
|
||||||
|
class="upload-button"
|
||||||
|
:disabled="isUploading"
|
||||||
|
>
|
||||||
|
Upload File
|
||||||
|
</button>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="error-message">
|
<div v-if="isUploading" class="upload-status">
|
||||||
✗ {{ errorMessage }}
|
<div class="spinner"></div>
|
||||||
</div>
|
<p>Uploading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="uploadSuccess" class="success-message">
|
||||||
|
✓ File uploaded successfully!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="error-message">✗ {{ errorMessage }}</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
uploadUrl: string;
|
uploadUrl: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
additionalData?: Record<string, string>;
|
additionalData?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'doError', error: Error): void;
|
(e: "doError", error: Error): void;
|
||||||
(e: 'doFileSelected', file: File): void;
|
(e: "doFileSelected", file: File): void;
|
||||||
(e: 'doSuccess', response: any): void;
|
(e: "doSuccess", response: any): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
headers: () => ({}),
|
headers: () => ({}),
|
||||||
additionalData: () => ({})
|
additionalData: () => ({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
@ -212,89 +223,89 @@ const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
const selectedFile = ref<File | null>(null);
|
const selectedFile = ref<File | null>(null);
|
||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
const uploadSuccess = ref(false);
|
const uploadSuccess = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref("");
|
||||||
|
|
||||||
const handleFileSelect = (event: Event) => {
|
const handleFileSelect = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
// Validate it's a CSV file
|
// Validate it's a CSV file
|
||||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
if (!file.name.toLowerCase().endsWith(".csv")) {
|
||||||
errorMessage.value = 'Please select a valid CSV file';
|
errorMessage.value = "Please select a valid CSV file";
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedFile.value = file;
|
selectedFile.value = file;
|
||||||
errorMessage.value = '';
|
errorMessage.value = "";
|
||||||
uploadSuccess.value = false;
|
uploadSuccess.value = false;
|
||||||
emit('doFileSelected', file);
|
emit("doFileSelected", file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async () => {
|
const uploadFile = async () => {
|
||||||
if (!selectedFile.value) return;
|
if (!selectedFile.value) return;
|
||||||
|
|
||||||
isUploading.value = true;
|
isUploading.value = true;
|
||||||
errorMessage.value = '';
|
errorMessage.value = "";
|
||||||
uploadSuccess.value = false;
|
uploadSuccess.value = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', selectedFile.value);
|
formData.append("file", selectedFile.value);
|
||||||
|
|
||||||
// Add any additional data to the form
|
|
||||||
Object.entries(props.additionalData).forEach(([key, value]) => {
|
|
||||||
formData.append(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(props.uploadUrl, {
|
// Add any additional data to the form
|
||||||
body: formData,
|
Object.entries(props.additionalData).forEach(([key, value]) => {
|
||||||
headers: {
|
formData.append(key, value);
|
||||||
...props.headers,
|
});
|
||||||
// Don't set Content-Type - let browser set it with boundary
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const response = await fetch(props.uploadUrl, {
|
||||||
throw new Error(`Upload failed: ${response.statusText}`);
|
body: formData,
|
||||||
}
|
headers: {
|
||||||
|
...props.headers,
|
||||||
|
// Don't set Content-Type - let browser set it with boundary
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
uploadSuccess.value = true;
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
emit('doSuccess', data);
|
}
|
||||||
|
|
||||||
resetUpload();
|
const data = await response.json();
|
||||||
} catch (error) {
|
uploadSuccess.value = true;
|
||||||
errorMessage.value = error instanceof Error ? error.message : 'Upload failed';
|
emit("doSuccess", data);
|
||||||
emit('doError', error as Error);
|
|
||||||
} finally {
|
resetUpload();
|
||||||
isUploading.value = false;
|
} catch (error) {
|
||||||
}
|
errorMessage.value =
|
||||||
|
error instanceof Error ? error.message : "Upload failed";
|
||||||
|
emit("doError", error as Error);
|
||||||
|
} finally {
|
||||||
|
isUploading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetUpload = () => {
|
const resetUpload = () => {
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
uploadSuccess.value = false;
|
uploadSuccess.value = false;
|
||||||
errorMessage.value = '';
|
errorMessage.value = "";
|
||||||
if (fileInput.value) {
|
if (fileInput.value) {
|
||||||
fileInput.value.value = '';
|
fileInput.value.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose methods for parent component
|
// Expose methods for parent component
|
||||||
defineExpose({
|
defineExpose({
|
||||||
resetUpload
|
resetUpload,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedCommunication" class="h-100 d-flex flex-column">
|
<div v-if="selectedCommunication" class="h-100 d-flex flex-column">
|
||||||
<PublicreportCard :report="selectedCommunication.public_report" @viewImage="openPhotoViewer" />
|
<PublicreportCard
|
||||||
|
:report="selectedCommunication.public_report"
|
||||||
|
@viewImage="openPhotoViewer"
|
||||||
|
/>
|
||||||
<!-- Report Details -->
|
<!-- Report Details -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<p>A flyover pool</p>
|
<p>A flyover pool</p>
|
||||||
<MapProxiedArcgisTile
|
<MapProxiedArcgisTile
|
||||||
id="tile-map"
|
id="tile-map"
|
||||||
:latitude="pool.location.latitude"
|
:latitude="pool.location.latitude"
|
||||||
:longitude="pool.location.longitude"
|
:longitude="pool.location.longitude"
|
||||||
:markers="tileMapMarkers"
|
:markers="tileMapMarkers"
|
||||||
:organizationId="user.organization.id"
|
:organizationId="user.organization.id"
|
||||||
:tegola="user.urls.tegola" />
|
:tegola="user.urls.tegola"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1>Map failed to load</h1>
|
<h1>Map failed to load</h1>
|
||||||
<p>{{error}}</p>
|
<p>{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
import {
|
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
} from "vue";
|
|
||||||
import { Bounds, Marker } from "@/types";
|
import { Bounds, Marker } from "@/types";
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1>Map failed to load</h1>
|
<h1>Map failed to load</h1>
|
||||||
<p>{{error}}</p>
|
<p>{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { Point } from "@/types";
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "map-click", latitude: Number, longitude: Number): void
|
(e: "map-click", latitude: Number, longitude: Number): void;
|
||||||
}
|
}
|
||||||
interface Props {
|
interface Props {
|
||||||
location: Point;
|
location: Point;
|
||||||
|
|
@ -40,8 +40,8 @@ const props = defineProps<Props>();
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const mapContainer = ref<HTMLElement | null>(null);
|
const mapContainer = ref<HTMLElement | null>(null);
|
||||||
const map = ref<maplibregl.Map | null>(null);
|
const map = ref<maplibregl.Map | null>(null);
|
||||||
const markerInstances = ref<Map<string, maplibrgl.Marker>>(new Map())
|
const markerInstances = ref<Map<string, maplibrgl.Marker>>(new Map());
|
||||||
const markers = ref<Map<string, maplibrgl.Marker>>(new Map())
|
const markers = ref<Map<string, maplibrgl.Marker>>(new Map());
|
||||||
|
|
||||||
// Watch for latitude/longitude changes
|
// Watch for latitude/longitude changes
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -108,7 +108,7 @@ const initializeMap = () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.error("hey dummy", e);
|
console.error("hey dummy", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,22 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="text-muted small mb-2">Lead → Field Assignment</div>
|
<div class="text-muted small mb-2">Lead → Field Assignment</div>
|
||||||
<button class="btn btn-outline-success tool-button" @click="emit('doCreateProposedAssignment')">
|
<button
|
||||||
|
class="btn btn-outline-success tool-button"
|
||||||
|
@click="emit('doCreateProposedAssignment')"
|
||||||
|
>
|
||||||
Create Proposed Assignment
|
Create Proposed Assignment
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary tool-button" @click="emit('doAddLeadsToAssignment')">
|
<button
|
||||||
|
class="btn btn-outline-secondary tool-button"
|
||||||
|
@click="emit('doAddLeadsToAssignment')"
|
||||||
|
>
|
||||||
Add Leads to Existing Assignment
|
Add Leads to Existing Assignment
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary tool-button" @click="emit('doSplitLead')">
|
<button
|
||||||
|
class="btn btn-outline-secondary tool-button"
|
||||||
|
@click="emit('doSplitLead')"
|
||||||
|
>
|
||||||
Split Lead
|
Split Lead
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,11 +71,22 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="text-muted small mb-2">Assignment Controls</div>
|
<div class="text-muted small mb-2">Assignment Controls</div>
|
||||||
<button class="btn btn-outline-dark tool-button" @click="emit('doSetPriority')">Set Priority</button>
|
<button
|
||||||
<button class="btn btn-outline-dark tool-button" @click="emit('doEstimateEffort')">
|
class="btn btn-outline-dark tool-button"
|
||||||
|
@click="emit('doSetPriority')"
|
||||||
|
>
|
||||||
|
Set Priority
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-dark tool-button"
|
||||||
|
@click="emit('doEstimateEffort')"
|
||||||
|
>
|
||||||
Estimate Effort
|
Estimate Effort
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-dark tool-button" @click="emit('doSendToOperations')">
|
<button
|
||||||
|
class="btn btn-outline-dark tool-button"
|
||||||
|
@click="emit('doSendToOperations')"
|
||||||
|
>
|
||||||
Send to Operations
|
Send to Operations
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,10 @@
|
||||||
Click signals from the left panel to select them
|
Click signals from the left panel to select them
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="mt-2" v-show="selectedSignals.length > 0">
|
||||||
class="mt-2"
|
<div v-for="signal in selectedSignals" :key="signal.id">
|
||||||
v-show="selectedSignals.length > 0"
|
<PlanningColumnDetailEntry :signal="signal" />
|
||||||
>
|
</div>
|
||||||
<div v-for="signal in selectedSignals" :key="signal.id">
|
|
||||||
<PlanningColumnDetailEntry :signal="signal"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,18 +120,24 @@ const selectedSignalLocation = () => {
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, null);
|
}, null);
|
||||||
const loc = first_pool?.location;
|
const loc = first_pool?.location;
|
||||||
return loc || {
|
return (
|
||||||
latitude: 0,
|
loc || {
|
||||||
longitude: 0,
|
latitude: 0,
|
||||||
}
|
longitude: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const showMapTile = () => {
|
const showMapTile = () => {
|
||||||
return selectedSignalLocation() && props.selectedSignals.value
|
return (
|
||||||
.values()
|
selectedSignalLocation() &&
|
||||||
.reduce(
|
props.selectedSignals.value
|
||||||
(accumulator, current) => accumulator || current.type === "flyover pool",
|
.values()
|
||||||
false,
|
.reduce(
|
||||||
);
|
(accumulator, current) =>
|
||||||
|
accumulator || current.type === "flyover pool",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const updateSignalLocation = (event) => {
|
const updateSignalLocation = (event) => {
|
||||||
const signalId = event.detail.signalId;
|
const signalId = event.detail.signalId;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
<TimeRelative :time="signal.created"></TimeRelative>
|
<TimeRelative :time="signal.created"></TimeRelative>
|
||||||
<p>{{ shortAddress(signal.address) }}</p>
|
<p>{{ shortAddress(signal.address) }}</p>
|
||||||
<div v-if="signal.type == 'flyover pool' && signal.pool">
|
<div v-if="signal.type == 'flyover pool' && signal.pool">
|
||||||
<FlyoverPoolCard :pool="signal.pool"/>
|
<FlyoverPoolCard :pool="signal.pool" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="signal.type == 'publicreport nuisance'">
|
<div v-else-if="signal.type == 'publicreport nuisance'">
|
||||||
<PublicreportCard :report="signal.report"/>
|
<PublicreportCard :report="signal.report" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="signal.type == 'publicreport water'">
|
<div v-else-if="signal.type == 'publicreport water'">
|
||||||
<PublicreportCard :report="signal.report"/>
|
<PublicreportCard :report="signal.report" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,10 @@
|
||||||
:class="{ selected: isSelected(signal.id) }"
|
:class="{ selected: isSelected(signal.id) }"
|
||||||
@click="toggleSignal(signal)"
|
@click="toggleSignal(signal)"
|
||||||
>
|
>
|
||||||
<PlanningColumnListEntry :selected="selectedSignalIDs.has(signal.id)" :signal="signal"/>
|
<PlanningColumnListEntry
|
||||||
|
:selected="selectedSignalIDs.has(signal.id)"
|
||||||
|
:signal="signal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import { shortAddress } from "../format";
|
||||||
interface Props {
|
interface Props {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
signal: Signal;
|
signal: Signal;
|
||||||
};
|
}
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
function icon(signal: Signal): string {
|
function icon(signal: Signal): string {
|
||||||
if (signal.type == "flyover pool") {
|
if (signal.type == "flyover pool") {
|
||||||
|
|
@ -34,7 +34,7 @@ function icon(signal: Signal): string {
|
||||||
return "bi-mosquito";
|
return "bi-mosquito";
|
||||||
} else if (signal.type == "publicreport water") {
|
} else if (signal.type == "publicreport water") {
|
||||||
return "bi-water";
|
return "bi-water";
|
||||||
} else {
|
} else {
|
||||||
return "bi-mosquito";
|
return "bi-mosquito";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,16 @@
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-1">
|
<h5 class="mb-1">
|
||||||
<span
|
<span v-if="report.type === 'nuisance'">
|
||||||
v-if="
|
|
||||||
report.type === 'nuisance'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i class="bi bi-mosquito icon-nuisance"></i>
|
<i class="bi bi-mosquito icon-nuisance"></i>
|
||||||
Nuisance Report
|
Nuisance Report
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-if="report.type === 'water'">
|
||||||
v-if="report.type === 'water'"
|
|
||||||
>
|
|
||||||
<i class="bi bi-droplet-fill icon-standing-water"></i>
|
<i class="bi bi-droplet-fill icon-standing-water"></i>
|
||||||
Standing Water Report
|
Standing Water Report
|
||||||
</span>
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
<small class="text-muted"
|
<small class="text-muted">Report ID: #{{ report.public_id }}</small>
|
||||||
>Report ID: #{{ report.public_id }}</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="badge bg-secondary">
|
<span class="badge bg-secondary">
|
||||||
<TimeRelative :time="report.created" />
|
<TimeRelative :time="report.created" />
|
||||||
|
|
@ -49,11 +41,7 @@
|
||||||
<i class="bi bi-geo-alt"></i> Address
|
<i class="bi bi-geo-alt"></i> Address
|
||||||
</label>
|
</label>
|
||||||
<div class="fw-medium">
|
<div class="fw-medium">
|
||||||
{{
|
{{ formatAddress(report.address) }}
|
||||||
formatAddress(
|
|
||||||
report.address,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
@ -61,25 +49,18 @@
|
||||||
<i class="bi bi-person"></i> Reporter Name
|
<i class="bi bi-person"></i> Reporter Name
|
||||||
</label>
|
</label>
|
||||||
<div class="fw-medium">
|
<div class="fw-medium">
|
||||||
{{
|
{{ report.reporter.name || "not given" }}
|
||||||
report.reporter.name ||
|
|
||||||
"not given"
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label
|
<label
|
||||||
v-if="
|
v-if="report.reporter.has_email"
|
||||||
report.reporter.has_email
|
|
||||||
"
|
|
||||||
class="form-label text-muted small mb-0"
|
class="form-label text-muted small mb-0"
|
||||||
>
|
>
|
||||||
<i class="bi bi-envelope"></i>
|
<i class="bi bi-envelope"></i>
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="
|
v-if="report.reporter.has_phone"
|
||||||
report.reporter.has_phone
|
|
||||||
"
|
|
||||||
class="form-label text-muted small mb-0"
|
class="form-label text-muted small mb-0"
|
||||||
>
|
>
|
||||||
<i class="bi bi-phone"></i>
|
<i class="bi bi-phone"></i>
|
||||||
|
|
@ -126,9 +107,7 @@
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if="report.nuisance?.is_location_backyard">Backyard</li>
|
<li v-if="report.nuisance?.is_location_backyard">Backyard</li>
|
||||||
<li v-if="report.nuisance?.is_location_frontyard">
|
<li v-if="report.nuisance?.is_location_frontyard">Frontyard</li>
|
||||||
Frontyard
|
|
||||||
</li>
|
|
||||||
<li v-if="report.nuisance?.is_location_garden">Garden</li>
|
<li v-if="report.nuisance?.is_location_garden">Garden</li>
|
||||||
<li v-if="report.nuisance?.is_location_other">Other</li>
|
<li v-if="report.nuisance?.is_location_other">Other</li>
|
||||||
<li v-if="report.nuisance?.is_location_pool">Pool</li>
|
<li v-if="report.nuisance?.is_location_pool">Pool</li>
|
||||||
|
|
@ -245,9 +224,7 @@
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="bi"
|
class="bi"
|
||||||
:class="
|
:class="report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'"
|
||||||
report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'
|
|
||||||
"
|
|
||||||
></i>
|
></i>
|
||||||
Pupae
|
Pupae
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -259,9 +236,7 @@
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="bi"
|
class="bi"
|
||||||
:class="
|
:class="report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'"
|
||||||
report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'
|
|
||||||
"
|
|
||||||
></i>
|
></i>
|
||||||
Adult Mosquitoes
|
Adult Mosquitoes
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -311,10 +286,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="report.images && report.images.length > 0"
|
||||||
report.images &&
|
|
||||||
report.images.length > 0
|
|
||||||
"
|
|
||||||
class="d-flex flex-wrap gap-2"
|
class="d-flex flex-wrap gap-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
@ -327,10 +299,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="!report.images || report.images.length === 0"
|
||||||
!report.images ||
|
|
||||||
report.images.length === 0
|
|
||||||
"
|
|
||||||
class="text-muted text-center py-3"
|
class="text-muted text-center py-3"
|
||||||
>
|
>
|
||||||
<i class="bi bi-camera-slash fs-4"></i>
|
<i class="bi bi-camera-slash fs-4"></i>
|
||||||
|
|
|
||||||
43
ts/router.ts
43
ts/router.ts
|
|
@ -154,11 +154,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
meta: { requiresAuth: true, showSidebar: true },
|
meta: { requiresAuth: true, showSidebar: true },
|
||||||
},
|
},
|
||||||
// Catch-all route - must be last
|
// Catch-all route - must be last
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: "/:pathMatch(.*)*",
|
||||||
name: 'NotFound',
|
name: "NotFound",
|
||||||
component: NotFound
|
component: NotFound,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|
@ -168,23 +168,22 @@ export const router = createRouter({
|
||||||
|
|
||||||
// Global navigation guard
|
// Global navigation guard
|
||||||
router.beforeEach(async (to, from) => {
|
router.beforeEach(async (to, from) => {
|
||||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
|
||||||
|
|
||||||
if (requiresAuth) {
|
if (requiresAuth) {
|
||||||
try {
|
try {
|
||||||
// Check if user is authenticated (could be an API call)
|
// Check if user is authenticated (could be an API call)
|
||||||
const isAuthenticated = await apiClient.isAuthenticated();
|
const isAuthenticated = await apiClient.isAuthenticated();
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return '/signin';
|
return "/signin";
|
||||||
} else {
|
} else {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("check auth failed");
|
console.log("check auth failed");
|
||||||
return '/signin';
|
return "/signin";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ function updateMap() {
|
||||||
location: {
|
location: {
|
||||||
lng: loc.longitude,
|
lng: loc.longitude,
|
||||||
lat: loc.latitude,
|
lat: loc.latitude,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
console.log("markers now", mapMarkers.value);
|
console.log("markers now", mapMarkers.value);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<template>
|
<template>
|
||||||
<p>No idea where you wanted to go with that one.</p>
|
<p>No idea where you wanted to go with that one.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.login-container {
|
.login-container {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.login-box {
|
.login-box {
|
||||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.login-form-section {
|
.login-form-section {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
.product-info-section {
|
.product-info-section {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
.login-header {
|
.login-header {
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -61,7 +61,6 @@
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Login</button>
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -167,10 +167,7 @@
|
||||||
available to Nidus
|
available to Nidus
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<a
|
<a class="btn btn-outline-warning" href="/configuration/upload">
|
||||||
class="btn btn-outline-warning"
|
|
||||||
href="/configuration/upload"
|
|
||||||
>
|
|
||||||
Manage Uploads
|
Manage Uploads
|
||||||
<i class="bi bi-arrow-right ms-1"></i>
|
<i class="bi bi-arrow-right ms-1"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@
|
||||||
</p>
|
</p>
|
||||||
<RouterLink to="/configuration/upload/pool">
|
<RouterLink to="/configuration/upload/pool">
|
||||||
<button class="btn btn-primary">
|
<button class="btn btn-primary">
|
||||||
<i class="bi bi-upload me-2"></i>Upload Green Pool Data</button
|
<i class="bi bi-upload me-2"></i>Upload Green Pool Data
|
||||||
>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-white text-muted">
|
<div class="card-footer bg-white text-muted">
|
||||||
|
|
@ -134,24 +134,24 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="upload in uploads">
|
<tr v-for="upload in uploads">
|
||||||
<td><TimeRelative :time="upload.created"/></td>
|
<td><TimeRelative :time="upload.created" /></td>
|
||||||
<td>{{upload.type}}</td>
|
<td>{{ upload.type }}</td>
|
||||||
<td>{{upload.filename}}</td>
|
<td>{{ upload.filename }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="upload.status"
|
<span class="badge" :class="upload.status">{{
|
||||||
>{{upload.status}}</span
|
upload.status
|
||||||
>
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{upload.record_count}} entries</td>
|
<td>{{ upload.record_count }} entries</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
class="btn btn-sm btn-outline-primary"
|
class="btn btn-sm btn-outline-primary"
|
||||||
:href="`/configuration/upload/${upload.id}`"
|
:href="`/configuration/upload/${upload.id}`"
|
||||||
>View</a
|
>View</a
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.upload-card {
|
.upload-card {
|
||||||
transition:
|
transition:
|
||||||
transform 0.2s,
|
transform 0.2s,
|
||||||
box-shadow 0.2s;
|
box-shadow 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.upload-card:hover {
|
.upload-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.card-icon {
|
.card-icon {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
|
|
@ -50,12 +50,9 @@
|
||||||
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<RouterLink
|
<RouterLink to="/configuration/upload/pool/flyover">
|
||||||
to="/configuration/upload/pool/flyover"
|
<button class="btn btn-primary btn-lg w-100">
|
||||||
>
|
<i class="bi bi-upload me-2"></i>Let's do this
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-lg w-100">
|
|
||||||
<i class="bi bi-upload me-2"></i>Let's do this
|
|
||||||
</button>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -85,11 +82,8 @@
|
||||||
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<RouterLink
|
<RouterLink to="/configuration/upload/pool/custom">
|
||||||
to="/configuration/upload/pool/custom"
|
<button class="btn btn-success btn-lg w-100">
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-success btn-lg w-100">
|
|
||||||
<i class="bi bi-upload me-2"></i>Pick me
|
<i class="bi bi-upload me-2"></i>Pick me
|
||||||
</button>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<CSVUpload
|
<CSVUpload
|
||||||
upload-url="/api/upload/pool/flyover"
|
upload-url="/api/upload/pool/flyover"
|
||||||
@doError="onError"
|
@doError="onError"
|
||||||
@doFileSelected="onFileSelected"
|
@doFileSelected="onFileSelected"
|
||||||
@doSuccess="onUploadSuccess" />
|
@doSuccess="onUploadSuccess"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
127
vite.config.ts
127
vite.config.ts
|
|
@ -3,69 +3,74 @@ import vue from "@vitejs/plugin-vue";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./ts"),
|
"@": path.resolve(__dirname, "./ts"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
api: "modern-compiler",
|
api: "modern-compiler",
|
||||||
silenceDeprecations: ["import", "global-builtin", "if-function", "color-functions"],
|
silenceDeprecations: [
|
||||||
},
|
"import",
|
||||||
},
|
"global-builtin",
|
||||||
},
|
"if-function",
|
||||||
|
"color-functions",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
manifest: true,
|
manifest: true,
|
||||||
outDir: "static/gen",
|
outDir: "static/gen",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: path.resolve(__dirname, "ts/main.ts"),
|
main: path.resolve(__dirname, "ts/main.ts"),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: "js/bundle.[hash].js",
|
entryFileNames: "js/bundle.[hash].js",
|
||||||
chunkFileNames: "js/[name].[hash].js",
|
chunkFileNames: "js/[name].[hash].js",
|
||||||
assetFileNames: (assetInfo) => {
|
assetFileNames: (assetInfo) => {
|
||||||
if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name || "")) {
|
if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name || "")) {
|
||||||
return "fonts/[name].[hash][extname]";
|
return "fonts/[name].[hash][extname]";
|
||||||
}
|
}
|
||||||
if (/\.css$/.test(assetInfo.name || "")) {
|
if (/\.css$/.test(assetInfo.name || "")) {
|
||||||
return "css/style.[hash][extname]";
|
return "css/style.[hash][extname]";
|
||||||
}
|
}
|
||||||
return "assets/[name].[hash][extname]";
|
return "assets/[name].[hash][extname]";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ["poweredge.local", "dev-sync.nidus.cloud"],
|
allowedHosts: ["poweredge.local", "dev-sync.nidus.cloud"],
|
||||||
port: 9000,
|
port: 9000,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://127.0.0.1:9002",
|
target: "http://127.0.0.1:9002",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
},
|
},
|
||||||
"/configuration/upload/pool/flyover": {
|
"/configuration/upload/pool/flyover": {
|
||||||
target: "http://127.0.0.1:9002",
|
target: "http://127.0.0.1:9002",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
},
|
},
|
||||||
"/signin": {
|
"/signin": {
|
||||||
target: "http://localhost:9002",
|
target: "http://localhost:9002",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
},
|
},
|
||||||
"/signup": {
|
"/signup": {
|
||||||
target: "http://localhost:9002",
|
target: "http://localhost:9002",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue