Pretty all the things I missed

My laptop didn't have lefthook running. Oops.
This commit is contained in:
Eli Ribble 2026-03-27 14:06:50 -07:00
parent f60bde7fd9
commit 4bbfbdb9e6
No known key found for this signature in database
30 changed files with 490 additions and 487 deletions

View file

@ -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)

View file

@ -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")

View file

@ -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
} }

View file

@ -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

View file

@ -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)
} }

View file

@ -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 {

View file

@ -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
} }

View file

@ -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,

View file

@ -1,4 +1,3 @@
package sync package sync
import ( import ()
)

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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";

View file

@ -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);
} }
}; };

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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";
} }
} }

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,
}, },
}); });