Begin ripping apart the communications page into components

Essential to get the logic under control
This commit is contained in:
Eli Ribble 2026-03-22 02:37:10 +00:00
parent ef552af054
commit d9a98e9eb2
No known key found for this signature in database
3 changed files with 73 additions and 173 deletions

View file

@ -1,43 +1,45 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { useUserStore } from "./user";
export const useCommunicationStore = defineStore("communication", () => {
// State
const communications = ref(null);
const all = ref(null);
const loading = ref(false);
const error = ref(null);
// Actions
async function fetchCommunications() {
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);
//if (typeFilter.value) params.append("type", typeFilter.value);
const response = await fetch(
`$${apiBase.value}/communication?$${params}`,
`${userStore.urls.api.communication}?${params}`,
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
communications.value = data.communications;
// if we already had something selected, reset it using the new data
if (selectedCommunication.value) {
const matching = communications.value.filter((report) => {
return report.id === selectedCommunication.value.id;
});
if (matching.length > 0) {
selectedCommunication.value = matching[0];
}
}
all.value = data.communications;
} catch (err) {
console.error("Error loading communications:", err);
throw err;
}
}
return {
// State
all,
// Actions
fetch,
};
});

View file

@ -1,22 +1,48 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
// Define interfaces matching your Go structs
interface URLsAPI {
communication: string;
}
interface URLs {
api: URLsAPI;
tegola: string;
}
interface User {
display_name: string;
initials: string;
notification_counts: NotificationCounts;
notifications: any[]; // Replace with proper type
organization: string; // Replace with proper type
role: string;
username: string;
}
interface UserResponse {
self: User;
urls: URLs;
}
interface NotificationCounts {
// Add the actual structure based on your API
[key: string]: number;
}
export const useUserStore = defineStore("user", () => {
// State
const display_name = ref(null);
const error = ref(null);
const initials = ref(null);
const display_name = ref<string | null>(null);
const error = ref<string | null>(null);
const initials = ref<string | null>(null);
const loading = ref(false);
const notification_counts = ref(null);
const notifications = ref(null);
const organization = ref(null);
const role = ref(null);
const urls = ref(null);
const username = ref(null);
// Getters
const isAuthenticated = computed(() => user.value !== null);
const userName = computed(() => user.value?.name ?? "");
const notification_counts = ref<NotificationCounts | null>(null);
const notifications = ref<any[] | null>(null);
const organization = ref<string | null>(null);
const role = ref<string | null>(null);
const urls = ref<URLs | null>(null);
const username = ref<string | null>(null);
// Actions
async function fetchUser() {
@ -27,7 +53,7 @@ export const useUserStore = defineStore("user", () => {
const response = await fetch("/api/user/self");
if (!response.ok) throw new Error("Failed to fetch user");
const data = await response.json();
const data: UserResponse = await response.json();
display_name.value = data.self.display_name;
initials.value = data.self.initials;
notification_counts.value = data.self.notification_counts;
@ -38,17 +64,13 @@ export const useUserStore = defineStore("user", () => {
username.value = data.self.username;
console.log("loaded user data", data);
} catch (e) {
error.value = e.message;
error.value = e instanceof Error ? e.message : "an error ocurred";
console.error("Error fetching user:", e);
} finally {
loading.value = false;
}
}
function clearUser() {
user.value = null;
}
return {
// State
display_name,
@ -61,11 +83,7 @@ export const useUserStore = defineStore("user", () => {
role,
urls,
username,
// Getters
isAuthenticated,
userName,
// Actions
fetchUser,
clearUser,
};
});

View file

@ -3,128 +3,7 @@
<div class="container-fluid h-100">
<div class="row h-100">
<!-- Left Column - Communications List -->
<div class="col-md-3 border-end p-0 reports-list">
<div class="p-3 bg-light border-bottom">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input
type="text"
class="form-control"
placeholder="Filter reports..."
v-model="searchFilter"
/>
</div>
<div class="mt-2 d-flex gap-2">
<button
class="btn btn-sm"
:class="
typeFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'
"
@click="typeFilter = 'all'"
>
All
</button>
<button
class="btn btn-sm"
:class="
typeFilter === 'nuisance'
? 'btn-danger'
: 'btn-outline-secondary'
"
@click="typeFilter = 'nuisance'"
>
<i class="bi bi-mosquito"></i>Mosquito Nuisance
</button>
<button
class="btn btn-sm"
:class="
typeFilter === 'water' ? 'btn-info' : 'btn-outline-secondary'
"
@click="typeFilter = 'water'"
>
<i class="bi bi-droplet"></i> Water
</button>
</div>
</div>
<div class="list-group list-group-flush">
<div
v-for="comm in filteredCommunications"
:key="comm.id"
class="list-group-item report-card p-3"
:class="{
active:
selectedCommunication && selectedCommunication.id === comm.id,
}"
@click="selectCommunication(comm)"
>
<!-- First row: icon, type badge, and time -->
<div
class="d-flex justify-content-between align-items-center mb-2"
>
<div class="d-flex align-items-center">
<i
v-if="comm.type === 'publicreport.nuisance'"
class="bi bi-mosquito icon-nuisance fs-4 me-2"
>
</i>
<i
v-if="comm.type === 'publicreport.water'"
class="bi bi-droplet-fill icon-standing-water fs-4 me-2"
></i>
<span
class="badge"
:class="
comm.type === 'publicreport.nuisance'
? 'bg-danger'
: 'bg-info'
"
>
{{
comm.type === "publicreport.nuisance"
? "Nuisance"
: "Standing Water"
}}
</span>
</div>
<small>
<TimeRelative :time="comm.created" />
</small>
</div>
<!-- Details section: full width -->
<div>
<div>
<i class="bi bi-geo-alt text-muted"></i>
<span class="fw-medium">{{
comm.public_report.address.postal_code
}}</span>
</div>
<small>{{ formatAddress(comm.public_report.address) }}</small>
<div
v-if="
comm.public_report.images &&
comm.public_report.images.length > 0
"
class="mt-1"
>
<small class="text-muted">
<i class="bi bi-camera"></i>
{{ comm.public_report.images.length }} photo(s)
</small>
</div>
</div>
</div>
</div>
<div
v-if="filteredCommunications.length === 0"
class="text-center text-muted p-4"
>
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">No reports found</p>
</div>
</div>
<CommunicationsColumnList all="communication.all" />
<!-- Middle Column - Report Details -->
<div class="col-md-6 p-0">
@ -781,16 +660,15 @@ import { useUserStore } from "../store/user";
import MapMultipoint from "../components/MapMultipoint.vue";
import TimeRelative from "../components/TimeRelative.vue";
const communicationStore = useCommunicationStore();
const communication = useCommunicationStore();
const user = useUserStore();
onMounted(() => {
communicationStore.fetchCommunications();
communication.fetch();
});
// Refs
const apiBase = ref("/api");
const selectedCommunication = ref(null);
const searchFilter = ref("");
const typeFilter = ref("all");
const messageText = ref("");
const showPhotoModal = ref(false);
@ -798,20 +676,10 @@ const currentPhotoIndex = ref(0);
const showToast = ref(false);
const toastTitle = ref("");
const toastMessage = ref("");
const communications = ref([]);
const loading = ref(false);
const error = ref(null);
const mapRef = ref(null);
// Computed properties
const filteredCommunications = computed(() => {
return communications.value.filter((report) => {
const matchesType =
typeFilter.value === "all" || report.type === typeFilter.value;
return matchesType && filterMatches(searchFilter.value, report);
});
});
const nuisance = computed(() => {
return selectedCommunication.value?.public_report?.nuisance || null;
});
@ -820,6 +688,18 @@ const water = computed(() => {
return selectedCommunication.value?.public_report?.water || null;
});
async function fetchCommunications() {
await communication.fetchAll();
// if we already had something selected, reset it using the new data
if (selectedCommunication.value) {
const matching = communication.all.filter((c) => {
return c.id === selectedCommunication.value.id;
});
if (matching.length > 0) {
selectedCommunication.value = matching[0];
}
}
}
// Methods
function filterMatches(filter, comm) {
// Implement your filter logic here