Get some planning buttons wired up
This commit is contained in:
parent
ef412b28ec
commit
bf2a7582fa
12 changed files with 568 additions and 25 deletions
|
|
@ -13,5 +13,9 @@
|
|||
import { useUserStore } from "../store/user";
|
||||
import MapProxiedArcgisTile from "@/components/MapProxiedArcgisTile.vue";
|
||||
|
||||
interface Props {
|
||||
pool: Pool;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const user = useUserStore();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ const markers = ref<Map<string, maplibrgl.Marker>>(new Map())
|
|||
|
||||
// Watch for latitude/longitude changes
|
||||
watch(
|
||||
() => [props.latitude, props.longitude],
|
||||
([newLat, newLng]) => {
|
||||
() => [props.location],
|
||||
([newLocation]) => {
|
||||
if (map.value) {
|
||||
map.value.jumpTo({
|
||||
center: [newLng, newLat],
|
||||
center: [newLocation.longitude, newLocation.latitude],
|
||||
zoom: 19,
|
||||
});
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ const initializeMap = () => {
|
|||
|
||||
try {
|
||||
map.value = new maplibregl.Map({
|
||||
center: [props.longitude, props.latitude],
|
||||
center: [props.location.longitude, props.location.latitude],
|
||||
container: mapContainer.value,
|
||||
style: "https://tiles.stadiamaps.com/styles/osm_bright.json",
|
||||
zoom: 19,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<button
|
||||
class="btn btn-outline-primary tool-button"
|
||||
:disabled="selectedSignalIDs.size === 0 || creating"
|
||||
@click="createLead()"
|
||||
@click="emit('doCreateLead')"
|
||||
>
|
||||
<span v-if="!creating">Create New Lead from Selection</span>
|
||||
<span v-else>
|
||||
|
|
@ -30,13 +30,14 @@
|
|||
<button
|
||||
class="btn btn-outline-secondary tool-button"
|
||||
:disabled="selectedSignalIDs.size === 0"
|
||||
@click="emit('doAddToLead')"
|
||||
>
|
||||
Add Signals to Existing Lead
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-secondary tool-button"
|
||||
:disabled="selectedSignalIDs.size === 0"
|
||||
@click="markAsAddressed()"
|
||||
@click="emit('doMarkSignalAddressed')"
|
||||
>
|
||||
Mark Signal as Addressed
|
||||
</button>
|
||||
|
|
@ -46,13 +47,13 @@
|
|||
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Lead → Field Assignment</div>
|
||||
<button class="btn btn-outline-success tool-button">
|
||||
<button class="btn btn-outline-success tool-button" @click="emit('doCreateProposedAssignment')">
|
||||
Create Proposed Assignment
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary tool-button">
|
||||
<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">
|
||||
<button class="btn btn-outline-secondary tool-button" @click="emit('doSplitLead')">
|
||||
Split Lead
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -61,11 +62,11 @@
|
|||
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Assignment Controls</div>
|
||||
<button class="btn btn-outline-dark tool-button">Set Priority</button>
|
||||
<button class="btn btn-outline-dark tool-button">
|
||||
<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">
|
||||
<button class="btn btn-outline-dark tool-button" @click="emit('doSendToOperations')">
|
||||
Send to Operations
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -73,9 +74,21 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
interface Emits {
|
||||
(e: "doAddToLead"): void;
|
||||
(e: "doAddLeadsToAssignment"): void;
|
||||
(e: "doCreateLead"): void;
|
||||
(e: "doCreateProposedAssignment"): void;
|
||||
(e: "doEstimateEffort"): void;
|
||||
(e: "doMarkSignalAddressed"): void;
|
||||
(e: "doSetPriority"): void;
|
||||
(e: "doSendToOperations"): void;
|
||||
(e: "doSplitLead"): void;
|
||||
}
|
||||
interface Props {
|
||||
creating: boolean;
|
||||
selectedSignalIDs: Set<int>;
|
||||
}
|
||||
const emit = defineEmits<Emits>();
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -70,8 +70,7 @@
|
|||
:organization-id="user.organization.id"
|
||||
:tegola="user.urls.tegola"
|
||||
:url-tiles="user.urls.tile"
|
||||
:latitude="selectedSignalLocation()?.latitude ?? 0.0"
|
||||
:longitude="selectedSignalLocation()?.longitude ?? 0.0"
|
||||
:location="selectedSignalLocation()"
|
||||
@map-click="updateSignalLocation"
|
||||
>
|
||||
</MapProxiedArcgisTile>
|
||||
|
|
@ -131,10 +130,14 @@ const selectedSignalLocation = () => {
|
|||
}
|
||||
return accumulator;
|
||||
}, null);
|
||||
return first_pool?.location;
|
||||
const loc = first_pool?.location;
|
||||
return loc || {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
}
|
||||
};
|
||||
const showMapTile = () => {
|
||||
return props.selectedSignals.value
|
||||
return selectedSignalLocation() && props.selectedSignals.value
|
||||
.values()
|
||||
.reduce(
|
||||
(accumulator, current) => accumulator || current.type === "flyover pool",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<TimeRelative :time="signal.created"></TimeRelative>
|
||||
<p>{{ shortAddress(signal.address) }}</p>
|
||||
<div v-if="signal.type == 'flyover pool'">
|
||||
<div v-if="signal.type == 'flyover pool' && signal.pool">
|
||||
<FlyoverPoolCard :pool="signal.pool"/>
|
||||
</div>
|
||||
<div v-else-if="signal.type == 'publicreport nuisance'">
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
:class="{ selected: isSelected(signal.id) }"
|
||||
@click="toggleSignal(signal)"
|
||||
>
|
||||
<PlanningColumnListEntry :signal="signal"/>
|
||||
<PlanningColumnListEntry :selected="selectedSignalIDs.has(signal.id)" :signal="signal"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<style scoped>
|
||||
.selected {
|
||||
background-color: $primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="row" :class="selected ? 'selected' : ''">
|
||||
<div class="col-1">
|
||||
<i class="bi" :class="icon(signal)"></i>
|
||||
</div>
|
||||
|
|
@ -17,6 +23,7 @@
|
|||
<script setup lang="ts">
|
||||
import { shortAddress } from "../format";
|
||||
interface Props {
|
||||
selected: boolean;
|
||||
signal: Signal;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
|
|
|
|||
18
ts/router.ts
18
ts/router.ts
|
|
@ -10,6 +10,7 @@ import ConfigurationPesticide from "./view/configuration/Pesticide.vue";
|
|||
import ConfigurationPesticideAdd from "./view/configuration/PesticideAdd.vue";
|
||||
import ConfigurationRoot from "./view/configuration/Root.vue";
|
||||
import ConfigurationUpload from "./view/configuration/Upload.vue";
|
||||
import ConfigurationUploadDetail from "./view/configuration/UploadDetail.vue";
|
||||
import ConfigurationUploadPool from "./view/configuration/UploadPool.vue";
|
||||
import ConfigurationUploadPoolFlyover from "./view/configuration/UploadPoolFlyover.vue";
|
||||
import ConfigurationUser from "./view/configuration/User.vue";
|
||||
|
|
@ -79,6 +80,13 @@ const routes: RouteRecordRaw[] = [
|
|||
component: ConfigurationUpload,
|
||||
meta: { requiresAuth: true, showSidebar: true },
|
||||
},
|
||||
{
|
||||
component: ConfigurationUploadDetail,
|
||||
meta: { requiresAuth: true, showSidebar: true },
|
||||
name: "Upload Detail",
|
||||
path: "/configuration/upload/:id",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/configuration/upload/pool",
|
||||
name: "Pool Upload",
|
||||
|
|
@ -159,7 +167,7 @@ const router = createRouter({
|
|||
});
|
||||
|
||||
// Global navigation guard
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
||||
|
||||
if (requiresAuth) {
|
||||
|
|
@ -167,16 +175,14 @@ router.beforeEach(async (to, from, next) => {
|
|||
// Check if user is authenticated (could be an API call)
|
||||
const isAuthenticated = await apiClient.isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
next('/signin');
|
||||
return '/signin';
|
||||
} else {
|
||||
next();
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("check auth failed");
|
||||
next('/signin');
|
||||
return '/signin';
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
60
ts/store/upload.ts
Normal file
60
ts/store/upload.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { Upload } from "../types";
|
||||
import { SSEManager } from "../SSEManager";
|
||||
import { useUserStore } from "./user";
|
||||
|
||||
export const useUploadStore = defineStore("upload", () => {
|
||||
// State
|
||||
const all = ref<Upload[] | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Subscription
|
||||
SSEManager.subscribe("*", (e) => {
|
||||
if (e.resource.startsWith("upload")) {
|
||||
fetchAll();
|
||||
}
|
||||
});
|
||||
// Actions
|
||||
function byID(id: int): Upload? {
|
||||
if (all.value == null) {
|
||||
return null
|
||||
}
|
||||
return all.value.find((upload) => upload.id == id);
|
||||
}
|
||||
async function fetchAll() {
|
||||
const userStore = useUserStore();
|
||||
if (userStore.urls == null) {
|
||||
throw new Error("can't fetch without user URL data");
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append("sort", "-created");
|
||||
//if (typeFilter.value) params.append("type", typeFilter.value);
|
||||
|
||||
const response = await fetch(`${userStore.urls.api.upload}?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
all.value = data.uploads;
|
||||
} catch (err) {
|
||||
console.error("Error loading uploads:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
all,
|
||||
// Actions
|
||||
byID,
|
||||
fetchAll,
|
||||
};
|
||||
});
|
||||
|
|
@ -40,6 +40,15 @@
|
|||
</template>
|
||||
<template #right>
|
||||
<PlanningColumnAction
|
||||
@doAddToLead="doAddToLead"
|
||||
@doAddLeadsToAssignment="doAddLeadsToAssignment"
|
||||
@doCreateLead="doCreateLead"
|
||||
@doCreateProposedAssignment="doCreateProposedAssignment"
|
||||
@doEstimateEffort="doEstimateEffort"
|
||||
@doMarkSignalAddressed="doMarkSignalAddressed"
|
||||
@doSetPriority="doSetPriority"
|
||||
@doSendToOperations="doSendToOperations"
|
||||
@doSplitLead="doSplitLead"
|
||||
:creating="creating"
|
||||
:selectedSignalIDs="selectedSignalIDs"
|
||||
/>
|
||||
|
|
@ -74,6 +83,33 @@ const selectedSignalIDs = ref(new Set<int>([]));
|
|||
const signal = useSignalStore();
|
||||
const user = useUserStore();
|
||||
|
||||
function doAddToLead() {
|
||||
console.log("doAddToLead");
|
||||
}
|
||||
function doAddLeadsToAssignment() {
|
||||
console.log("doAddLeadsToAssignment");
|
||||
}
|
||||
function doCreateLead() {
|
||||
console.log("doCreateLead");
|
||||
}
|
||||
function doCreateProposedAssignment() {
|
||||
console.log("doCreateProposedAssignment");
|
||||
}
|
||||
function doEstimateEffort() {
|
||||
console.log("doEstimateEffort");
|
||||
}
|
||||
function doMarkSignalAddressed() {
|
||||
console.log("doMarkSignalAddressed");
|
||||
}
|
||||
function doSetPriority() {
|
||||
console.log("doSetPriority");
|
||||
}
|
||||
function doSendToOperations() {
|
||||
console.log("doSendToOperations");
|
||||
}
|
||||
function doSplitLead() {
|
||||
console.log("doSplitLead");
|
||||
}
|
||||
// Helper functions (outside component)
|
||||
const getBoundingBox = (points) => {
|
||||
if (!points || points.length === 0) {
|
||||
|
|
|
|||
410
ts/view/configuration/UploadDetail.vue
Normal file
410
ts/view/configuration/UploadDetail.vue
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
<style scoped>
|
||||
.results-container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.has-error {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
.badge.status,
|
||||
.badge.condition {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.35em 0.65em;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
thead tr.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container mt-4 results-container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Upload Results: {{ upload?.name }}</h2>
|
||||
<span class="badge rounded-pill" :class="upload?.status">
|
||||
<i class="bi me-1" :class="getUploadStatusIcon(upload?.status)"></i>
|
||||
{{ getUploadStatusDisplay(upload?.status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card summary-card h-100 border-primary">
|
||||
<div class="card-body text-center">
|
||||
<h1 class="display-4 text-primary">
|
||||
{{ upload?.countExisting }}
|
||||
</h1>
|
||||
<h5>Existing Pools</h5>
|
||||
<p class="text-muted">Matches found in previous records</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card summary-card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<h1 class="display-4 text-success">{{ upload?.countNew }}</h1>
|
||||
<h5>New Pools</h5>
|
||||
<p class="text-muted">Not found in existing records</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card summary-card h-100 border-warning">
|
||||
<div class="card-body text-center">
|
||||
<h1 class="display-4 text-warning">{{ upload?.countOutside }}</h1>
|
||||
<h5>Outside District</h5>
|
||||
<p class="text-muted">Potential geocoding errors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div v-if="user == null">
|
||||
<p>loading</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MapMultipoint
|
||||
id="map"
|
||||
:markers="[]"
|
||||
:organization-id="user.organization.id"
|
||||
:tegola="user.urls.tegola"
|
||||
:xmin="user?.organization?.serviceArea?.min.x ?? 0"
|
||||
:ymin="user?.organization?.serviceArea?.min.y ?? 0"
|
||||
:xmax="user?.organization?.serviceArea?.max.x ?? 0"
|
||||
:ymax="user?.organization?.serviceArea?.max.y ?? 0"
|
||||
></MapMultipoint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Data Preview</h5>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="showIssuesOnly"
|
||||
v-model="showIssuesOnly"
|
||||
@change="handleShowIssuesOnly"
|
||||
/>
|
||||
<label class="form-check-label" for="showIssuesOnly">
|
||||
Show issues only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div
|
||||
v-for="error in upload?.errors"
|
||||
:key="error.message"
|
||||
class="alert alert-danger"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Error:</strong> {{ error.message }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="upload?.status === 'uploaded' || upload?.status === 'parsing'"
|
||||
class="alert alert-info"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Working:</strong> File is still processing... refresh this
|
||||
page in a bit to see updates.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="!upload?.pools || upload.pools.length === 0"
|
||||
class="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> No pools could be understood from your file.
|
||||
</div>
|
||||
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead class="table-light">
|
||||
<tr class="header">
|
||||
<th></th>
|
||||
<th>Number</th>
|
||||
<th>Street</th>
|
||||
<th>City</th>
|
||||
<th>Post</th>
|
||||
<th>Status</th>
|
||||
<th>Condition</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(pool, index) in upload.pools"
|
||||
:key="index"
|
||||
:class="{ 'has-error': pool.errors && pool.errors.length > 0 }"
|
||||
:style="getRowStyle(pool)"
|
||||
>
|
||||
<td>
|
||||
<i
|
||||
v-if="pool.errors && pool.errors.length > 0"
|
||||
class="bi bi-info-circle-fill text-primary ms-1"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
:title="pool.errors.map(e => e.message).join(', ')"
|
||||
></i>
|
||||
</td>
|
||||
<td>{{ pool.address.number }}</td>
|
||||
<td>{{ pool.address.street }}</td>
|
||||
<td>{{ pool.address.locality }}</td>
|
||||
<td>{{ pool.address.postalCode }}</td>
|
||||
<td>
|
||||
<span class="badge status" :class="pool.status">
|
||||
{{ titleCase(pool.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge condition" :class="pool.condition">
|
||||
{{ titleCase(pool.condition) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ pool.tags?.length || 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 mb-5">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-danger"
|
||||
@click="handleDiscard"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
id="confirmUploadBtn"
|
||||
@click="handleConfirm"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<i class="bi bi-check2 me-1"></i> Confirm and Submit Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import MapMultipoint from "@/components/MapMultipoint.vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
|
||||
interface Address {
|
||||
number: string;
|
||||
street: string;
|
||||
locality: string;
|
||||
postalCode: string;
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface Pool {
|
||||
address: Address;
|
||||
status: string;
|
||||
condition: string;
|
||||
tags?: string[];
|
||||
errors?: ErrorMessage[];
|
||||
}
|
||||
|
||||
interface Upload {
|
||||
name: string;
|
||||
status: string;
|
||||
countExisting: number;
|
||||
countNew: number;
|
||||
countOutside: number;
|
||||
errors?: ErrorMessage[];
|
||||
pools?: Pool[];
|
||||
}
|
||||
|
||||
interface ServiceArea {
|
||||
min: { x: number; y: number };
|
||||
max: { x: number; y: number };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: int;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const router = useRouter();
|
||||
const showIssuesOnly = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const user = useUserStore();
|
||||
|
||||
const getUploadStatusIcon = (status?: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
uploaded: 'bi-cloud-upload',
|
||||
parsing: 'bi-hourglass-split',
|
||||
parsed: 'bi-check-circle',
|
||||
error: 'bi-x-circle',
|
||||
};
|
||||
return icons[status || ''] || 'bi-question-circle';
|
||||
};
|
||||
|
||||
const getUploadStatusDisplay = (status?: string): string => {
|
||||
const displays: Record<string, string> = {
|
||||
uploaded: 'Uploaded',
|
||||
parsing: 'Parsing',
|
||||
parsed: 'Parsed',
|
||||
error: 'Error',
|
||||
};
|
||||
return displays[status || ''] || 'Unknown';
|
||||
};
|
||||
|
||||
const titleCase = (str?: string): string => {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
};
|
||||
const upload = computed(() => {
|
||||
if (upload.all == null) {
|
||||
return null;
|
||||
}
|
||||
return upload.byID(props.id);
|
||||
});
|
||||
const getRowStyle = (pool: Pool) => {
|
||||
if (showIssuesOnly.value) {
|
||||
const hasError = pool.errors && pool.errors.length > 0;
|
||||
return { display: hasError ? 'table-row' : 'none' };
|
||||
}
|
||||
return { display: 'table-row' };
|
||||
};
|
||||
|
||||
const handleShowIssuesOnly = () => {
|
||||
// The reactive display is handled by getRowStyle
|
||||
};
|
||||
|
||||
const initializeMap = () => {
|
||||
if (!map) return;
|
||||
|
||||
map.addEventListener('load', () => {
|
||||
map.addSource('tegola-nidus', {
|
||||
type: 'vector',
|
||||
tiles: [
|
||||
`${props.tegolaUrl}maps/nidus/{z}/{x}/{y}?csv_file=${props.id}&id=${user.organization.id}`,
|
||||
],
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: 'pool',
|
||||
source: 'tegola-nidus',
|
||||
'source-layer': 'fileupload-pool',
|
||||
type: 'circle',
|
||||
paint: {
|
||||
'circle-color': '#91b979',
|
||||
'circle-radius': 7,
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#7aab5f',
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUploadData = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/upload/${props.id}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch upload data');
|
||||
upload.value = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching upload data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = async () => {
|
||||
if (!confirm('Are you sure you want to discard this upload?')) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const response = await fetch(`/api/upload/${props.id}/discard`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to discard upload');
|
||||
|
||||
// Navigate to uploads list or appropriate page
|
||||
router.push('/uploads');
|
||||
} catch (error) {
|
||||
console.error('Error discarding upload:', error);
|
||||
alert('Failed to discard upload. Please try again.');
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const response = await fetch(`/api/upload/${props.id}/commit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to confirm upload');
|
||||
|
||||
// Navigate to success page or appropriate page
|
||||
router.push('/uploads/success');
|
||||
} catch (error) {
|
||||
console.error('Error confirming upload:', error);
|
||||
alert('Failed to confirm upload. Please try again.');
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeMap();
|
||||
|
||||
// If upload data wasn't provided via props, fetch it
|
||||
if (!upload.value) {
|
||||
fetchUploadData();
|
||||
}
|
||||
|
||||
// Initialize Bootstrap tooltips
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
// @ts-ignore - Bootstrap types
|
||||
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
});
|
||||
</script>
|
||||
|
|
@ -53,6 +53,10 @@ export default defineConfig({
|
|||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/configuration/upload/pool/flyover": {
|
||||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/signin": {
|
||||
target: "http://localhost:9002",
|
||||
changeOrigin: false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue