Begin ripping apart the communications page into components
Essential to get the logic under control
This commit is contained in:
parent
ef552af054
commit
d9a98e9eb2
3 changed files with 73 additions and 173 deletions
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue