nidus-sync/ts/components/PlanningColumnList.vue

229 lines
5.6 KiB
Vue

<style scoped>
.error-message {
background-color: #f8d7da;
border: 1px solid #f5c2c7;
border-radius: 0.25rem;
padding: 1rem;
margin-bottom: 1rem;
color: #842029;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.signal-address {
font-size: 0.875rem;
color: #6c757d;
}
.signal-item {
cursor: pointer;
transition: all 0.2s;
}
.signal-item:hover {
background-color: #f8f9fa;
}
.signal-item.selected {
background-color: #e7f3ff;
border-color: #0d6efd;
}
</style>
<template>
<div class="card shadow-sm h-100">
<div class="card-header bg-white pane-header">
Incoming Signals & Leads
<span
v-show="loading"
class="spinner-border spinner-border-sm ms-2"
role="status"
></span>
</div>
<div class="card-body scroll-pane">
<!-- Error Display -->
<div v-if="error" class="error-message">
<strong>Error:</strong> <span>{{ error }}</span>
<button
@click="loadData()"
class="btn btn-sm btn-outline-danger mt-2 w-100"
>
Retry
</button>
</div>
<!-- FILTERS -->
<div class="mb-3">
<div class="filter-label mb-1">Species</div>
<select
class="form-select form-select-sm mb-2 disabled"
disabled
v-model="filters.species"
@change="loadSignals()"
>
<option value="">All Species</option>
<option value="aedes_aegypti">Aedes aegypti</option>
<option value="aedes_albopictus">Aedes albopictus</option>
<option value="culex_pipiens">Culex pipiens</option>
<option value="culex_tarsalis">Culex tarsalis</option>
</select>
<div class="filter-label mb-1">Signal Type</div>
<select
class="form-select form-select-sm mb-2 disabled"
disabled
v-model="filters.type"
@change="loadSignals()"
>
<option value="">All Types</option>
<option value="public_report">Public Report</option>
<option value="trap_spike">Trap Spike</option>
<option value="surveillance">Surveillance Observation</option>
<option value="residual_expiring">Residual Expiring</option>
<option value="plan_followup">Plan Follow-Up</option>
</select>
<div class="filter-label mb-1">Sort By</div>
<select
class="form-select form-select-sm disabled"
disabled
v-model="filters.sort"
@change="loadSignals()"
>
<option value="newest">Newest First</option>
<option value="priority">Highest Priority</option>
<option value="linked">Most Signals Linked</option>
<option value="species_signal">Strongest Species Signal</option>
</select>
</div>
<hr />
<!-- Loading State -->
<div v-if="loading" class="loading-spinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else>
<div class="mb-3">
<div class="fw-semibold mb-2">
Signals
<span class="badge bg-primary" v-show="selectedSignals.length > 0">
{{ selectedSignals.length }}
</span>
</div>
<div
v-if="signals != null && signals.length === 0"
class="text-muted small fst-italic"
>
No signals found
</div>
<div
v-for="signal in signals"
v-if="signals != null && signals.length > 0"
:key="signal.id"
class="border rounded p-2 mb-2 signal-item"
:class="{ selected: isSelected(signal.id) }"
@click="toggleSignal(signal)"
>
<div class="small fw-semibold">{{ signal.title }}</div>
<div class="signal-address">
{{ shortAddress(signal.address) }}
</div>
<div class="text-muted small">{{ signal.description }}</div>
<span v-if="signal.badge" class="badge bg-secondary mt-1">
{{ signal.badge }}
</span>
</div>
</div>
</div>
<hr />
<!-- Mosquito Control Plan Followups -->
<div class="mb-3" v-show="!loading || planFollowups.length > 0">
<div class="fw-semibold mb-2">Mosquito Control Plan Follow-Ups</div>
<div
v-if="planFollowups.length === 0 && !loading"
class="text-muted small fst-italic"
>
No plan follow-ups
</div>
<div
v-for="followup in planFollowups"
:key="followup.id"
class="border rounded p-2 mb-2 signal-item"
:class="{ selected: isSelected(followup.id) }"
@click="toggleSignal(followup)"
>
<div class="small fw-semibold">{{ followup.title }}</div>
<div class="text-muted small">{{ followup.description }}</div>
<span class="badge bg-secondary">Plan</span>
</div>
</div>
<hr />
<!-- Leads -->
<div v-show="!loading || leads.length > 0">
<div class="fw-semibold mb-2">Existing Leads</div>
<div
v-if="leads.length === 0 && !loading"
class="text-muted small fst-italic"
>
No existing leads
</div>
<div
v-for="lead in leads"
:key="lead.id"
class="border rounded p-2 mb-2 signal-item"
>
<div class="small fw-semibold">{{ lead.title }}</div>
<div class="text-muted small">{{ lead.description }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface Props {
error: string | null;
leads: Lead[] | null;
loading: boolean;
planFollowups: Followup[] | null;
selectedSignals: int[];
signals: Signal[] | null;
}
const props = defineProps<Props>();
const filters = ref({
species: "",
type: "",
sort: "newest",
});
const isSelected = (id) => {
return props.selectedSignals.some((s) => s.id === id);
};
const shortAddress = (a) => {
if (!a) return "";
return `${a.number} ${a.street}, ${a.locality}`;
};
</script>