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

@ -1,24 +1,24 @@
// src/api/axios.js or similar
import axios from 'axios';
import router from '@/router';
import axios from "axios";
import router from "@/router";
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true
baseURL: "/api",
withCredentials: true,
});
// Response interceptor to catch auth failures
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Session expired or not authenticated
router.push('/login');
}
return Promise.reject(error);
}
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Session expired or not authenticated
router.push("/login");
}
return Promise.reject(error);
},
);
apiClient.isAuthenticated = () => {
return true;
}
};
export default apiClient;

View file

@ -1,209 +1,220 @@
<style scoped>
.upload-widget {
max-width: 400px;
font-family: system-ui, -apple-system, sans-serif;
max-width: 400px;
font-family:
system-ui,
-apple-system,
sans-serif;
}
.upload-area {
position: relative;
border: 2px dashed #cbd5e0;
border-radius: 8px;
padding: 2rem;
text-align: center;
background-color: #f7fafc;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
border: 2px dashed #cbd5e0;
border-radius: 8px;
padding: 2rem;
text-align: center;
background-color: #f7fafc;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #4299e1;
background-color: #ebf8ff;
border-color: #4299e1;
background-color: #ebf8ff;
}
.file-input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.file-input:disabled {
cursor: not-allowed;
cursor: not-allowed;
}
.upload-info {
pointer-events: none;
pointer-events: none;
}
.placeholder {
color: #718096;
color: #718096;
}
.placeholder svg {
margin: 0 auto 1rem;
color: #a0aec0;
margin: 0 auto 1rem;
color: #a0aec0;
}
.placeholder p {
margin: 0;
font-size: 0.875rem;
margin: 0;
font-size: 0.875rem;
}
.file-selected {
color: #2d3748;
color: #2d3748;
}
.file-name {
margin: 0 0 0.5rem;
font-weight: 600;
word-break: break-all;
margin: 0 0 0.5rem;
font-weight: 600;
word-break: break-all;
}
.file-size {
margin: 0;
font-size: 0.875rem;
color: #718096;
margin: 0;
font-size: 0.875rem;
color: #718096;
}
.upload-button {
width: 100%;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
width: 100%;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.upload-button:hover {
background-color: #3182ce;
background-color: #3182ce;
}
.upload-button:disabled {
background-color: #cbd5e0;
cursor: not-allowed;
background-color: #cbd5e0;
cursor: not-allowed;
}
.upload-status {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 1rem;
padding: 0.75rem;
background-color: #ebf8ff;
border-radius: 6px;
color: #2c5282;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 1rem;
padding: 0.75rem;
background-color: #ebf8ff;
border-radius: 6px;
color: #2c5282;
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid #bee3f8;
border-top-color: #4299e1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
width: 20px;
height: 20px;
border: 3px solid #bee3f8;
border-top-color: #4299e1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.success-message {
margin-top: 1rem;
padding: 0.75rem;
background-color: #c6f6d5;
color: #22543d;
border-radius: 6px;
font-weight: 500;
margin-top: 1rem;
padding: 0.75rem;
background-color: #c6f6d5;
color: #22543d;
border-radius: 6px;
font-weight: 500;
}
.error-message {
margin-top: 1rem;
padding: 0.75rem;
background-color: #fed7d7;
color: #742a2a;
border-radius: 6px;
font-weight: 500;
margin-top: 1rem;
padding: 0.75rem;
background-color: #fed7d7;
color: #742a2a;
border-radius: 6px;
font-weight: 500;
}
</style>
<template>
<div class="upload-widget">
<div class="upload-area">
<input
ref="fileInput"
type="file"
accept=".csv"
@change="handleFileSelect"
:disabled="isUploading"
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>
<div class="upload-widget">
<div class="upload-area">
<input
ref="fileInput"
type="file"
accept=".csv"
@change="handleFileSelect"
:disabled="isUploading"
class="file-input"
/>
<button
v-if="selectedFile && !isUploading"
@click="uploadFile"
class="upload-button"
:disabled="isUploading"
>
Upload File
</button>
<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-if="isUploading" class="upload-status">
<div class="spinner"></div>
<p>Uploading...</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>
<div v-if="uploadSuccess" class="success-message">
File uploaded successfully!
</div>
<button
v-if="selectedFile && !isUploading"
@click="uploadFile"
class="upload-button"
:disabled="isUploading"
>
Upload File
</button>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
<div v-if="isUploading" class="upload-status">
<div class="spinner"></div>
<p>Uploading...</p>
</div>
<div v-if="uploadSuccess" class="success-message">
File uploaded successfully!
</div>
<div v-if="errorMessage" class="error-message"> {{ errorMessage }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref } from "vue";
interface Props {
uploadUrl: string;
headers?: Record<string, string>;
additionalData?: Record<string, string>;
uploadUrl: string;
headers?: Record<string, string>;
additionalData?: Record<string, string>;
}
interface Emits {
(e: 'doError', error: Error): void;
(e: 'doFileSelected', file: File): void;
(e: 'doSuccess', response: any): void;
(e: "doError", error: Error): void;
(e: "doFileSelected", file: File): void;
(e: "doSuccess", response: any): void;
}
const props = withDefaults(defineProps<Props>(), {
headers: () => ({}),
additionalData: () => ({})
headers: () => ({}),
additionalData: () => ({}),
});
const emit = defineEmits<Emits>();
@ -212,89 +223,89 @@ const fileInput = ref<HTMLInputElement | null>(null);
const selectedFile = ref<File | null>(null);
const isUploading = ref(false);
const uploadSuccess = ref(false);
const errorMessage = ref('');
const errorMessage = ref("");
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
// Validate it's a CSV file
if (!file.name.toLowerCase().endsWith('.csv')) {
errorMessage.value = 'Please select a valid CSV file';
selectedFile.value = null;
return;
}
selectedFile.value = file;
errorMessage.value = '';
uploadSuccess.value = false;
emit('doFileSelected', file);
}
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
// Validate it's a CSV file
if (!file.name.toLowerCase().endsWith(".csv")) {
errorMessage.value = "Please select a valid CSV file";
selectedFile.value = null;
return;
}
selectedFile.value = file;
errorMessage.value = "";
uploadSuccess.value = false;
emit("doFileSelected", file);
}
};
const uploadFile = async () => {
if (!selectedFile.value) return;
if (!selectedFile.value) return;
isUploading.value = true;
errorMessage.value = '';
uploadSuccess.value = false;
isUploading.value = true;
errorMessage.value = "";
uploadSuccess.value = false;
try {
const formData = new FormData();
formData.append('file', selectedFile.value);
// Add any additional data to the form
Object.entries(props.additionalData).forEach(([key, value]) => {
formData.append(key, value);
});
try {
const formData = new FormData();
formData.append("file", selectedFile.value);
const response = await fetch(props.uploadUrl, {
body: formData,
headers: {
...props.headers,
// Don't set Content-Type - let browser set it with boundary
},
method: 'POST',
});
// Add any additional data to the form
Object.entries(props.additionalData).forEach(([key, value]) => {
formData.append(key, value);
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const response = await fetch(props.uploadUrl, {
body: formData,
headers: {
...props.headers,
// Don't set Content-Type - let browser set it with boundary
},
method: "POST",
});
const data = await response.json();
uploadSuccess.value = true;
emit('doSuccess', data);
resetUpload();
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Upload failed';
emit('doError', error as Error);
} finally {
isUploading.value = false;
}
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
uploadSuccess.value = true;
emit("doSuccess", data);
resetUpload();
} catch (error) {
errorMessage.value =
error instanceof Error ? error.message : "Upload failed";
emit("doError", error as Error);
} finally {
isUploading.value = false;
}
};
const resetUpload = () => {
selectedFile.value = null;
uploadSuccess.value = false;
errorMessage.value = '';
if (fileInput.value) {
fileInput.value.value = '';
}
selectedFile.value = null;
uploadSuccess.value = false;
errorMessage.value = "";
if (fileInput.value) {
fileInput.value.value = "";
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
};
// Expose methods for parent component
defineExpose({
resetUpload
resetUpload,
});
</script>

View file

@ -57,7 +57,10 @@
</div>
<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 -->
</div>
</div>

View file

@ -1,12 +1,13 @@
<template>
<p>A flyover pool</p>
<MapProxiedArcgisTile
id="tile-map"
:latitude="pool.location.latitude"
:longitude="pool.location.longitude"
:markers="tileMapMarkers"
:organizationId="user.organization.id"
:tegola="user.urls.tegola" />
id="tile-map"
:latitude="pool.location.latitude"
:longitude="pool.location.longitude"
:markers="tileMapMarkers"
:organizationId="user.organization.id"
:tegola="user.urls.tegola"
/>
</template>
<script setup lang="ts">

View file

@ -15,18 +15,13 @@
</div>
<div v-else>
<h1>Map failed to load</h1>
<p>{{error}}</p>
<p>{{ error }}</p>
</div>
</template>
<script setup lang="ts">
import "maplibre-gl/dist/maplibre-gl.css";
import {
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { Bounds, Marker } from "@/types";
import maplibregl from "maplibre-gl";

View file

@ -15,7 +15,7 @@
</div>
<div v-else>
<h1>Map failed to load</h1>
<p>{{error}}</p>
<p>{{ error }}</p>
</div>
</template>
@ -26,7 +26,7 @@ import { Point } from "@/types";
import maplibregl from "maplibre-gl";
interface Emits {
(e: "map-click", latitude: Number, longitude: Number): void
(e: "map-click", latitude: Number, longitude: Number): void;
}
interface Props {
location: Point;
@ -40,8 +40,8 @@ const props = defineProps<Props>();
const error = ref<string | null>(null);
const mapContainer = ref<HTMLElement | null>(null);
const map = ref<maplibregl.Map | null>(null);
const markerInstances = ref<Map<string, maplibrgl.Marker>>(new Map())
const markers = 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());
// Watch for latitude/longitude changes
watch(
@ -108,7 +108,7 @@ const initializeMap = () => {
});
});
});
} catch(e) {
} catch (e) {
console.error("hey dummy", e);
}
};

View file

@ -47,13 +47,22 @@
<div class="mb-3">
<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
</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
</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
</button>
</div>
@ -62,11 +71,22 @@
<div class="mb-3">
<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 class="btn btn-outline-dark tool-button" @click="emit('doEstimateEffort')">
<button
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
</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
</button>
</div>

View file

@ -42,13 +42,10 @@
Click signals from the left panel to select them
</div>
<div
class="mt-2"
v-show="selectedSignals.length > 0"
>
<div v-for="signal in selectedSignals" :key="signal.id">
<PlanningColumnDetailEntry :signal="signal"/>
</div>
<div class="mt-2" v-show="selectedSignals.length > 0">
<div v-for="signal in selectedSignals" :key="signal.id">
<PlanningColumnDetailEntry :signal="signal" />
</div>
</div>
</div>
</div>
@ -123,18 +120,24 @@ const selectedSignalLocation = () => {
return accumulator;
}, null);
const loc = first_pool?.location;
return loc || {
latitude: 0,
longitude: 0,
}
return (
loc || {
latitude: 0,
longitude: 0,
}
);
};
const showMapTile = () => {
return selectedSignalLocation() && props.selectedSignals.value
.values()
.reduce(
(accumulator, current) => accumulator || current.type === "flyover pool",
false,
);
return (
selectedSignalLocation() &&
props.selectedSignals.value
.values()
.reduce(
(accumulator, current) =>
accumulator || current.type === "flyover pool",
false,
)
);
};
const updateSignalLocation = (event) => {
const signalId = event.detail.signalId;

View file

@ -2,13 +2,13 @@
<TimeRelative :time="signal.created"></TimeRelative>
<p>{{ shortAddress(signal.address) }}</p>
<div v-if="signal.type == 'flyover pool' && signal.pool">
<FlyoverPoolCard :pool="signal.pool"/>
<FlyoverPoolCard :pool="signal.pool" />
</div>
<div v-else-if="signal.type == 'publicreport nuisance'">
<PublicreportCard :report="signal.report"/>
<PublicreportCard :report="signal.report" />
</div>
<div v-else-if="signal.type == 'publicreport water'">
<PublicreportCard :report="signal.report"/>
<PublicreportCard :report="signal.report" />
</div>
</template>

View file

@ -138,7 +138,10 @@
:class="{ selected: isSelected(signal.id) }"
@click="toggleSignal(signal)"
>
<PlanningColumnListEntry :selected="selectedSignalIDs.has(signal.id)" :signal="signal"/>
<PlanningColumnListEntry
:selected="selectedSignalIDs.has(signal.id)"
:signal="signal"
/>
</div>
</div>
</div>

View file

@ -25,7 +25,7 @@ import { shortAddress } from "../format";
interface Props {
selected: boolean;
signal: Signal;
};
}
const props = defineProps<Props>();
function icon(signal: Signal): string {
if (signal.type == "flyover pool") {
@ -34,7 +34,7 @@ function icon(signal: Signal): string {
return "bi-mosquito";
} else if (signal.type == "publicreport water") {
return "bi-water";
} else {
} else {
return "bi-mosquito";
}
}

View file

@ -16,24 +16,16 @@
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="mb-1">
<span
v-if="
report.type === 'nuisance'
"
>
<span v-if="report.type === 'nuisance'">
<i class="bi bi-mosquito icon-nuisance"></i>
Nuisance Report
</span>
<span
v-if="report.type === 'water'"
>
<span v-if="report.type === 'water'">
<i class="bi bi-droplet-fill icon-standing-water"></i>
Standing Water Report
</span>
</h5>
<small class="text-muted"
>Report ID: #{{ report.public_id }}</small
>
<small class="text-muted">Report ID: #{{ report.public_id }}</small>
</div>
<span class="badge bg-secondary">
<TimeRelative :time="report.created" />
@ -49,11 +41,7 @@
<i class="bi bi-geo-alt"></i> Address
</label>
<div class="fw-medium">
{{
formatAddress(
report.address,
)
}}
{{ formatAddress(report.address) }}
</div>
</div>
<div class="col-md-6">
@ -61,25 +49,18 @@
<i class="bi bi-person"></i> Reporter Name
</label>
<div class="fw-medium">
{{
report.reporter.name ||
"not given"
}}
{{ report.reporter.name || "not given" }}
</div>
</div>
<div class="col-md-6">
<label
v-if="
report.reporter.has_email
"
v-if="report.reporter.has_email"
class="form-label text-muted small mb-0"
>
<i class="bi bi-envelope"></i>
</label>
<label
v-if="
report.reporter.has_phone
"
v-if="report.reporter.has_phone"
class="form-label text-muted small mb-0"
>
<i class="bi bi-phone"></i>
@ -126,9 +107,7 @@
<div>
<ul>
<li v-if="report.nuisance?.is_location_backyard">Backyard</li>
<li v-if="report.nuisance?.is_location_frontyard">
Frontyard
</li>
<li v-if="report.nuisance?.is_location_frontyard">Frontyard</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_pool">Pool</li>
@ -245,9 +224,7 @@
>
<i
class="bi"
:class="
report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'
"
:class="report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'"
></i>
Pupae
</span>
@ -259,9 +236,7 @@
>
<i
class="bi"
:class="
report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'
"
:class="report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'"
></i>
Adult Mosquitoes
</span>
@ -311,10 +286,7 @@
</div>
<div class="card-body">
<div
v-if="
report.images &&
report.images.length > 0
"
v-if="report.images && report.images.length > 0"
class="d-flex flex-wrap gap-2"
>
<img
@ -327,10 +299,7 @@
/>
</div>
<div
v-if="
!report.images ||
report.images.length === 0
"
v-if="!report.images || report.images.length === 0"
class="text-muted text-center py-3"
>
<i class="bi bi-camera-slash fs-4"></i>

View file

@ -154,11 +154,11 @@ const routes: RouteRecordRaw[] = [
meta: { requiresAuth: true, showSidebar: true },
},
// Catch-all route - must be last
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: NotFound,
},
];
export const router = createRouter({
@ -168,23 +168,22 @@ export const router = createRouter({
// Global navigation guard
router.beforeEach(async (to, from) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth) {
try {
// Check if user is authenticated (could be an API call)
const isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
return '/signin';
} else {
return
}
} catch (error) {
console.log("check auth failed");
return '/signin';
}
}
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
if (requiresAuth) {
try {
// Check if user is authenticated (could be an API call)
const isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
return "/signin";
} else {
return;
}
} catch (error) {
console.log("check auth failed");
return "/signin";
}
}
});
export default router;

View file

@ -271,7 +271,7 @@ function updateMap() {
location: {
lng: loc.longitude,
lat: loc.latitude,
}
},
},
];
console.log("markers now", mapMarkers.value);

View file

@ -1,3 +1,3 @@
<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>

View file

@ -1,23 +1,23 @@
<style scoped>
.login-container {
max-width: 900px;
margin: 0 auto;
}
.login-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
overflow: hidden;
}
.login-form-section {
padding: 40px;
}
.product-info-section {
padding: 40px;
background-color: #f8f9fa;
}
.login-header {
margin-bottom: 25px;
}
.login-container {
max-width: 900px;
margin: 0 auto;
}
.login-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
overflow: hidden;
}
.login-form-section {
padding: 40px;
}
.product-info-section {
padding: 40px;
background-color: #f8f9fa;
}
.login-header {
margin-bottom: 25px;
}
</style>
<template>
@ -61,7 +61,6 @@
</div>
-->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>

View file

@ -167,10 +167,7 @@
available to Nidus
</p>
<div class="d-flex justify-content-between align-items-center">
<a
class="btn btn-outline-warning"
href="/configuration/upload"
>
<a class="btn btn-outline-warning" href="/configuration/upload">
Manage Uploads
<i class="bi bi-arrow-right ms-1"></i>
</a>

View file

@ -49,8 +49,8 @@
</p>
<RouterLink to="/configuration/upload/pool">
<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>
</div>
<div class="card-footer bg-white text-muted">
@ -134,24 +134,24 @@
</tr>
</thead>
<tbody>
<tr v-for="upload in uploads">
<td><TimeRelative :time="upload.created"/></td>
<td>{{upload.type}}</td>
<td>{{upload.filename}}</td>
<td>
<span class="badge" :class="upload.status"
>{{upload.status}}</span
>
</td>
<td>{{upload.record_count}} entries</td>
<td>
<a
class="btn btn-sm btn-outline-primary"
:href="`/configuration/upload/${upload.id}`"
>View</a
>
</td>
</tr>
<tr v-for="upload in uploads">
<td><TimeRelative :time="upload.created" /></td>
<td>{{ upload.type }}</td>
<td>{{ upload.filename }}</td>
<td>
<span class="badge" :class="upload.status">{{
upload.status
}}</span>
</td>
<td>{{ upload.record_count }} entries</td>
<td>
<a
class="btn btn-sm btn-outline-primary"
:href="`/configuration/upload/${upload.id}`"
>View</a
>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -1,20 +1,20 @@
<style scoped>
.upload-card {
transition:
transform 0.2s,
box-shadow 0.2s;
cursor: pointer;
height: 100%;
}
.upload-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
</style>
<style scoped>
.upload-card {
transition:
transform 0.2s,
box-shadow 0.2s;
cursor: pointer;
height: 100%;
}
.upload-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
</style>
<template>
<div class="container py-5">
<div class="row mb-4">
@ -50,12 +50,9 @@
<i class="bi bi-filetype-csv me-1"></i> CSV Format
</span>
</div>
<RouterLink
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
<RouterLink 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>
</RouterLink>
</div>
@ -85,11 +82,8 @@
<i class="bi bi-filetype-csv me-1"></i> CSV Format
</span>
</div>
<RouterLink
to="/configuration/upload/pool/custom"
>
<button
class="btn btn-success btn-lg w-100">
<RouterLink to="/configuration/upload/pool/custom">
<button class="btn btn-success btn-lg w-100">
<i class="bi bi-upload me-2"></i>Pick me
</button>
</RouterLink>

View file

@ -82,10 +82,11 @@
</div>
<div class="card-body">
<CSVUpload
upload-url="/api/upload/pool/flyover"
upload-url="/api/upload/pool/flyover"
@doError="onError"
@doFileSelected="onFileSelected"
@doSuccess="onUploadSuccess" />
@doSuccess="onUploadSuccess"
/>
</div>
</div>