big updates

This commit is contained in:
Casey 2025-11-21 12:29:31 -06:00
parent 34f2c110d6
commit 03a230b8f7
14 changed files with 2417 additions and 242 deletions

View file

@ -12,6 +12,9 @@ import {
Clock,
HistoricShield,
Developer,
Neighbourhood,
Calculator,
ReceiveDollars,
} from "@iconoir/vue";
import SpeedDial from "primevue/speeddial";
@ -37,7 +40,7 @@ const createButtons = ref([
{
label: "On-Site Meeting",
command: () => {
router.push("/onsitemeetings/new");
router.push("/schedule-onsite?new=true");
},
},
{
@ -72,7 +75,10 @@ const categories = ref([
{ name: "Home", icon: Home, url: "/" },
{ name: "Calendar", icon: Calendar, url: "/calendar" },
{ name: "Clients", icon: Community, url: "/clients" },
{ name: "On-Site Meetings", icon: Neighbourhood, url: "/schedule-onsite" },
{ name: "Estimates", icon: Calculator, url: "/estimates" },
{ name: "Jobs", icon: Hammer, url: "/jobs" },
{ name: "Payments/Invoices", icon: ReceiveDollars, url: "/invoices" },
{ name: "Routes", icon: PathArrowSolid, url: "/routes" },
{ name: "Time Sheets", icon: Clock, url: "/timesheets" },
{ name: "Warranties", icon: HistoricShield, url: "/warranties" },
@ -81,7 +87,7 @@ const categories = ref([
icon: MultiplePagesPlus,
buttons: createButtons,
},
{ name: "Development", icon: Developer, buttons: developmentButtons },
// { name: "Development", icon: Developer, buttons: developmentButtons },
]);
const handleCategoryClick = (category) => {
router.push(category.url);
@ -142,8 +148,11 @@ const handleCategoryClick = (category) => {
}
.button-icon {
justify-self: flex-start;
margin-left: 5px;
flex-shrink: 0;
width: 20px;
height: 20px;
margin-left: 8px;
margin-right: 8px;
}
.create-item {
@ -156,8 +165,14 @@ const handleCategoryClick = (category) => {
}
.button-text {
margin-left: auto;
margin-right: auto;
flex: 1;
text-align: center;
font-size: clamp(0.6rem, 2vw, 0.9rem);
white-space: nowrap;
padding-right: 8px;
line-height: 1.2;
font-weight: 500;
letter-spacing: -0.02em;
}
.sidebar-button {
@ -168,12 +183,17 @@ const handleCategoryClick = (category) => {
display: flex;
width: 100%;
align-items: center;
min-height: 44px;
height: 44px;
padding: 8px 0;
box-sizing: border-box;
}
#sidebar {
display: flex;
flex-direction: column;
width: 150px;
width: 180px;
min-width: 180px;
align-self: flex-start;
gap: 10px;
background-color: #f3f3f3;
@ -182,4 +202,32 @@ const handleCategoryClick = (category) => {
margin-top: 10px;
position: relative;
}
/* Responsive adjustments for smaller screens */
@media (max-width: 768px) {
#sidebar {
width: 160px;
min-width: 160px;
}
.button-text {
font-size: clamp(0.55rem, 1.8vw, 0.8rem);
}
}
@media (max-width: 480px) {
#sidebar {
width: 140px;
min-width: 140px;
}
.sidebar-button {
min-height: 40px;
height: 40px;
}
.button-text {
font-size: clamp(0.5rem, 1.5vw, 0.7rem);
}
}
</style>

View file

@ -272,6 +272,21 @@
:severity="getBadgeColor(slotProps.data[col.fieldName])"
/>
</template>
<template v-if="col.type === 'status-button'" #body="slotProps">
<Button
:label="slotProps.data[col.fieldName]"
:severity="getBadgeColor(slotProps.data[col.fieldName])"
size="small"
:variant="col.buttonVariant || 'filled'"
@click="handleStatusButtonClick(col, slotProps.data)"
:disabled="
loading ||
(col.disableCondition &&
col.disableCondition(slotProps.data[col.fieldName]))
"
class="status-button"
/>
</template>
<template v-if="col.type === 'date'" #body="slotProps">
<span>{{ formatDate(slotProps.data[col.fieldName]) }}</span>
</template>
@ -1044,6 +1059,17 @@ const handleBulkAction = (action, selectedRows) => {
}
};
// Handle status button clicks
const handleStatusButtonClick = (column, rowData) => {
try {
if (column.onStatusClick && typeof column.onStatusClick === "function") {
column.onStatusClick(rowData[column.fieldName], rowData);
}
} catch (error) {
console.error("Error executing status button click:", error);
}
};
const getBadgeColor = (status) => {
switch (status?.toLowerCase()) {
case "completed":
@ -1547,4 +1573,29 @@ defineExpose({
transform: translateX(0);
}
}
/* Status Button Styles */
.status-button {
font-weight: 500;
font-size: 0.8rem;
border-radius: 4px;
transition: all 0.2s ease;
min-width: 100px;
text-align: center;
}
.status-button:not(:disabled):hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status-button:disabled {
cursor: default;
opacity: 0.8;
}
.status-button:disabled:hover {
transform: none;
box-shadow: none;
}
</style>

View file

@ -1,286 +1,289 @@
<template>
<v-dialog
v-model="localVisible"
:persistent="options.persistent || false"
:fullscreen="options.fullscreen || false"
:max-width="options.maxWidth || '500px'"
:width="options.width"
:height="options.height"
:attach="options.attach"
:transition="options.transition || 'dialog-transition'"
:scrollable="options.scrollable || false"
:retain-focus="options.retainFocus !== false"
:close-on-back="options.closeOnBack !== false"
:close-on-content-click="options.closeOnContentClick || false"
:overlay-color="options.overlayColor"
:overlay-opacity="options.overlayOpacity"
:z-index="options.zIndex"
:class="options.dialogClass"
@click:outside="handleOutsideClick"
@keydown.esc="handleEscapeKey"
>
<v-card
:class="[
'modal-card',
options.cardClass,
{
'elevation-0': options.flat,
'rounded-0': options.noRadius
}
]"
:color="options.cardColor"
:variant="options.cardVariant"
:elevation="options.elevation"
>
<!-- Header Section -->
<v-card-title
v-if="options.showHeader !== false"
:class="[
'modal-header d-flex align-center justify-space-between',
options.headerClass
]"
>
<div class="modal-title">
<slot name="title">
{{ options.title }}
</slot>
</div>
<!-- Close button -->
<v-btn
v-if="options.showCloseButton !== false && !options.persistent"
icon
variant="text"
size="small"
:color="options.closeButtonColor || 'grey'"
@click="closeModal"
class="modal-close-btn"
>
<v-icon>{{ options.closeIcon || 'mdi-close' }}</v-icon>
</v-btn>
</v-card-title>
<v-dialog
v-model="localVisible"
:persistent="options.persistent || false"
:fullscreen="options.fullscreen || false"
:max-width="options.maxWidth || '500px'"
:width="options.width"
:height="options.height"
:attach="options.attach"
:transition="options.transition || 'dialog-transition'"
:scrollable="options.scrollable || false"
:retain-focus="options.retainFocus !== false"
:close-on-back="options.closeOnBack !== false"
:close-on-content-click="options.closeOnContentClick || false"
:overlay-color="options.overlayColor"
:overlay-opacity="options.overlayOpacity"
:z-index="options.zIndex"
:class="options.dialogClass"
@click:outside="handleOutsideClick"
@keydown.esc="handleEscapeKey"
>
<v-card
:class="[
'modal-card',
options.cardClass,
{
'elevation-0': options.flat,
'rounded-0': options.noRadius,
},
]"
:color="options.cardColor"
:variant="options.cardVariant"
:elevation="options.elevation"
>
<!-- Header Section -->
<v-card-title
v-if="options.showHeader !== false"
:class="[
'modal-header d-flex align-center justify-space-between',
options.headerClass,
]"
>
<div class="modal-title">
<slot name="title">
{{ options.title }}
</slot>
</div>
<v-divider v-if="options.showHeaderDivider && options.showHeader !== false" />
<!-- Close button -->
<v-btn
v-if="options.showCloseButton !== false && !options.persistent"
icon
variant="text"
size="small"
:color="options.closeButtonColor || 'grey'"
@click="closeModal"
class="modal-close-btn"
>
<v-icon>{{ options.closeIcon || "mdi-close" }}</v-icon>
</v-btn>
</v-card-title>
<!-- Content Section -->
<v-card-text
:class="[
'modal-content',
options.contentClass,
{
'pa-0': options.noPadding,
'overflow-y-auto': options.scrollable
}
]"
:style="contentStyle"
>
<slot>
<!-- Default content if no slot provided -->
<div v-if="options.message" v-html="options.message"></div>
</slot>
</v-card-text>
<v-divider v-if="options.showHeaderDivider && options.showHeader !== false" />
<!-- Actions Section -->
<v-card-actions
v-if="options.showActions !== false || $slots.actions"
:class="[
'modal-actions',
options.actionsClass,
{
'justify-end': options.actionsAlign === 'right',
'justify-center': options.actionsAlign === 'center',
'justify-start': options.actionsAlign === 'left',
'justify-space-between': options.actionsAlign === 'space-between'
}
]"
>
<slot name="actions" :close="closeModal" :options="options">
<!-- Default action buttons -->
<v-btn
v-if="options.showCancelButton !== false && !options.persistent"
:color="options.cancelButtonColor || 'grey'"
:variant="options.cancelButtonVariant || 'text'"
@click="handleCancel"
>
{{ options.cancelButtonText || 'Cancel' }}
</v-btn>
<v-btn
v-if="options.showConfirmButton !== false"
:color="options.confirmButtonColor || 'primary'"
:variant="options.confirmButtonVariant || 'elevated'"
:loading="options.loading"
@click="handleConfirm"
>
{{ options.confirmButtonText || 'Confirm' }}
</v-btn>
</slot>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Content Section -->
<v-card-text
:class="[
'modal-content',
options.contentClass,
{
'pa-0': options.noPadding,
'overflow-y-auto': options.scrollable,
},
]"
:style="contentStyle"
>
<slot>
<!-- Default content if no slot provided -->
<div v-if="options.message" v-html="options.message"></div>
</slot>
</v-card-text>
<!-- Actions Section -->
<v-card-actions
v-if="options.showActions !== false || $slots.actions"
:class="[
'modal-actions',
options.actionsClass,
{
'justify-end': options.actionsAlign === 'right',
'justify-center': options.actionsAlign === 'center',
'justify-start': options.actionsAlign === 'left',
'justify-space-between': options.actionsAlign === 'space-between',
},
]"
>
<slot name="actions" :close="closeModal" :options="options">
<!-- Default action buttons -->
<v-btn
v-if="options.showCancelButton !== false"
:color="options.cancelButtonColor || 'grey'"
:variant="options.cancelButtonVariant || 'text'"
@click="handleCancel"
>
{{ options.cancelButtonText || "Cancel" }}
</v-btn>
<v-btn
v-if="options.showConfirmButton !== false"
:color="options.confirmButtonColor || 'primary'"
:variant="options.confirmButtonVariant || 'elevated'"
:loading="options.loading"
@click="handleConfirm"
>
{{ options.confirmButtonText || "Confirm" }}
</v-btn>
</slot>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { computed, watch } from 'vue'
import { computed, watch } from "vue";
// Props
const props = defineProps({
// Modal visibility state
visible: {
type: Boolean,
default: false
},
// Options object for configuration
options: {
type: Object,
default: () => ({})
}
})
// Modal visibility state
visible: {
type: Boolean,
default: false,
},
// Options object for configuration
options: {
type: Object,
default: () => ({}),
},
});
// Emits
const emit = defineEmits([
'update:visible',
'close',
'confirm',
'cancel',
'outside-click',
'escape-key'
])
"update:visible",
"close",
"confirm",
"cancel",
"outside-click",
"escape-key",
]);
// Local visibility state that syncs with parent
const localVisible = computed({
get() {
return props.visible
},
set(value) {
emit('update:visible', value)
}
})
get() {
return props.visible;
},
set(value) {
emit("update:visible", value);
},
});
// Computed styles for content area
const contentStyle = computed(() => {
const styles = {}
if (props.options.contentHeight) {
styles.height = props.options.contentHeight
}
if (props.options.contentMaxHeight) {
styles.maxHeight = props.options.contentMaxHeight
}
if (props.options.contentMinHeight) {
styles.minHeight = props.options.contentMinHeight
}
return styles
})
const styles = {};
if (props.options.contentHeight) {
styles.height = props.options.contentHeight;
}
if (props.options.contentMaxHeight) {
styles.maxHeight = props.options.contentMaxHeight;
}
if (props.options.contentMinHeight) {
styles.minHeight = props.options.contentMinHeight;
}
return styles;
});
// Methods
const closeModal = () => {
localVisible.value = false
emit('close')
}
localVisible.value = false;
emit("close");
};
const handleConfirm = () => {
emit('confirm')
// Auto-close unless specified not to
if (props.options.autoCloseOnConfirm !== false) {
closeModal()
}
}
emit("confirm");
// Auto-close unless specified not to
if (props.options.autoCloseOnConfirm !== false) {
closeModal();
}
};
const handleCancel = () => {
emit('cancel')
// Auto-close unless specified not to
if (props.options.autoCloseOnCancel !== false) {
closeModal()
}
}
emit("cancel");
// Auto-close unless specified not to
if (props.options.autoCloseOnCancel !== false) {
closeModal();
}
};
const handleOutsideClick = () => {
emit('outside-click')
// Close on outside click unless persistent or disabled
if (!props.options.persistent && props.options.closeOnOutsideClick !== false) {
closeModal()
}
}
emit("outside-click");
// Close on outside click unless persistent or disabled
if (!props.options.persistent && props.options.closeOnOutsideClick !== false) {
closeModal();
}
};
const handleEscapeKey = () => {
emit('escape-key')
// Close on escape key unless persistent or disabled
if (!props.options.persistent && props.options.closeOnEscape !== false) {
closeModal()
}
}
emit("escape-key");
// Close on escape key unless persistent or disabled
if (!props.options.persistent && props.options.closeOnEscape !== false) {
closeModal();
}
};
// Watch for external visibility changes
watch(() => props.visible, (newValue) => {
if (newValue && props.options.onOpen) {
props.options.onOpen()
} else if (!newValue && props.options.onClose) {
props.options.onClose()
}
})
watch(
() => props.visible,
(newValue) => {
if (newValue && props.options.onOpen) {
props.options.onOpen();
} else if (!newValue && props.options.onClose) {
props.options.onClose();
}
},
);
</script>
<style scoped>
.modal-card {
position: relative;
position: relative;
}
.modal-header {
background-color: var(--v-theme-surface-variant);
padding: 16px 24px;
background-color: var(--v-theme-surface-variant);
padding: 16px 24px;
}
.modal-title {
font-size: 1.25rem;
font-weight: 500;
flex: 1;
font-size: 1.25rem;
font-weight: 500;
flex: 1;
}
.modal-close-btn {
flex-shrink: 0;
flex-shrink: 0;
}
.modal-content {
position: relative;
position: relative;
}
.modal-actions {
padding: 16px 24px;
gap: 8px;
padding: 16px 24px;
gap: 8px;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.modal-header {
padding: 12px 16px;
}
.modal-actions {
padding: 12px 16px;
}
.modal-title {
font-size: 1.1rem;
}
.modal-header {
padding: 12px 16px;
}
.modal-actions {
padding: 12px 16px;
}
.modal-title {
font-size: 1.1rem;
}
}
/* Custom transitions */
.v-dialog--fullscreen .modal-card {
height: 100vh;
border-radius: 0;
height: 100vh;
border-radius: 0;
}
/* Loading state */
.modal-card.loading {
pointer-events: none;
pointer-events: none;
}
</style>

View file

@ -0,0 +1,117 @@
<template>
<Modal v-model:visible="showModal" :options="modalOptions" @confirm="handleClose">
<template #title>Meeting Details</template>
<div v-if="meeting" class="meeting-details">
<div class="detail-row">
<v-icon class="mr-2">mdi-map-marker</v-icon>
<strong>Address:</strong> {{ meeting.address }}
</div>
<div class="detail-row" v-if="meeting.client">
<v-icon class="mr-2">mdi-account</v-icon>
<strong>Client:</strong> {{ meeting.client }}
</div>
<div class="detail-row">
<v-icon class="mr-2">mdi-calendar</v-icon>
<strong>Date:</strong> {{ formatDate(meeting.date) }}
</div>
<div class="detail-row">
<v-icon class="mr-2">mdi-clock</v-icon>
<strong>Time:</strong> {{ formatTimeDisplay(meeting.scheduledTime) }}
</div>
<div class="detail-row" v-if="meeting.duration">
<v-icon class="mr-2">mdi-timer</v-icon>
<strong>Duration:</strong> {{ meeting.duration }} minutes
</div>
<div class="detail-row" v-if="meeting.notes">
<v-icon class="mr-2">mdi-note-text</v-icon>
<strong>Notes:</strong> {{ meeting.notes }}
</div>
<div class="detail-row" v-if="meeting.status">
<v-icon class="mr-2">mdi-check-circle</v-icon>
<strong>Status:</strong> {{ meeting.status }}
</div>
</div>
</Modal>
</template>
<script setup>
import { computed } from "vue";
import Modal from "../common/Modal.vue";
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
meeting: {
type: Object,
default: null,
},
});
// Emits
const emit = defineEmits(["update:visible", "close"]);
// Local state
const showModal = computed({
get() {
return props.visible;
},
set(value) {
emit("update:visible", value);
},
});
// Modal options
const modalOptions = computed(() => ({
maxWidth: "600px",
showCancelButton: false,
confirmButtonText: "Close",
confirmButtonColor: "primary",
}));
// Methods
const handleClose = () => {
emit("close");
};
const formatTimeDisplay = (time) => {
if (!time) return "";
const [hours, minutes] = time.split(":").map(Number);
const displayHour = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
const ampm = hours >= 12 ? "PM" : "AM";
return `${displayHour}:${minutes.toString().padStart(2, "0")} ${ampm}`;
};
const formatDate = (dateStr) => {
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
</script>
<style scoped>
.meeting-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
}
.detail-row strong {
margin-right: 8px;
color: #333;
}
</style>

View file

@ -0,0 +1,311 @@
<template>
<!-- New Meeting Creation Modal -->
<Modal
v-model:visible="showModal"
:options="modalOptions"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<template #title>Schedule New On-Site Meeting</template>
<div class="new-meeting-form">
<div class="form-group">
<label for="meeting-address">Address: <span class="required">*</span></label>
<div class="address-input-group">
<InputText
id="meeting-address"
v-model="formData.address"
class="address-input"
placeholder="Enter meeting address"
@input="validateForm"
/>
<Button
label="Search"
icon="pi pi-search"
size="small"
:disabled="!formData.address.trim()"
@click="searchAddress"
class="search-btn"
/>
</div>
</div>
<div class="form-group">
<label for="meeting-notes">Notes (Optional):</label>
<Textarea
id="meeting-notes"
v-model="formData.notes"
class="w-full"
placeholder="Additional notes..."
rows="3"
/>
</div>
</div>
</Modal>
<!-- Address Search Results Modal -->
<Modal
v-model:visible="showAddressSearchModal"
:options="searchModalOptions"
@confirm="closeAddressSearch"
>
<template #title>Address Search Results</template>
<div class="address-search-results">
<div v-if="addressSearchResults.length === 0" class="no-results">
<i class="pi pi-info-circle"></i>
<p>No addresses found matching your search.</p>
</div>
<div v-else class="results-list">
<div
v-for="(address, index) in addressSearchResults"
:key="index"
class="address-result-item"
@click="selectAddress(address)"
>
<i class="pi pi-map-marker"></i>
<span>{{ address }}</span>
</div>
</div>
</div>
</Modal>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import Modal from "../common/Modal.vue";
import InputText from "primevue/inputtext";
import Textarea from "primevue/textarea";
import Button from "primevue/button";
import { useNotificationStore } from "../../stores/notifications-primevue";
import Api from "../../api";
const notificationStore = useNotificationStore();
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
initialAddress: {
type: String,
default: "",
},
});
// Emits
const emit = defineEmits(["update:visible", "confirm", "cancel"]);
// Local state
const showModal = computed({
get() {
return props.visible;
},
set(value) {
emit("update:visible", value);
},
});
const showAddressSearchModal = ref(false);
const addressSearchResults = ref([]);
const isFormValid = ref(false);
// Form data
const formData = ref({
address: "",
notes: "",
});
// Form validation state
// Modal options
const modalOptions = computed(() => ({
maxWidth: "500px",
persistent: true,
confirmButtonText: "Create",
cancelButtonText: "Cancel",
confirmButtonColor: "primary",
showConfirmButton: true,
showCancelButton: true,
confirmButtonProps: {
disabled: !isFormValid.value,
},
}));
const searchModalOptions = computed(() => ({
maxWidth: "600px",
showCancelButton: false,
confirmButtonText: "Close",
confirmButtonColor: "primary",
}));
// Methods
const validateForm = () => {
const hasValidAddress = formData.value.address && formData.value.address.trim().length > 0;
isFormValid.value = hasValidAddress;
};
const searchAddress = async () => {
const searchTerm = formData.value.address.trim();
if (!searchTerm) return;
try {
const results = await Api.searchAddresses(searchTerm);
console.info("Address search results:", results);
// Ensure results is always an array
// const safeResults = Array.isArray(results) ? results : [];
addressSearchResults.value = results;
if (results.length === 0) {
notificationStore.addWarning("No addresses found matching your search criteria.");
} else {
showAddressSearchModal.value = true;
}
} catch (error) {
console.error("Error searching addresses:", error);
addressSearchResults.value = [];
notificationStore.addError("Failed to search addresses. Please try again.");
}
};
const selectAddress = (address) => {
formData.value.address = address;
showAddressSearchModal.value = false;
validateForm();
};
const closeAddressSearch = () => {
showAddressSearchModal.value = false;
};
const handleConfirm = () => {
if (!isFormValid.value) return;
emit("confirm", { ...formData.value });
resetForm();
};
const handleCancel = () => {
emit("cancel");
resetForm();
};
const resetForm = () => {
formData.value = {
address: props.initialAddress || "",
notes: "",
};
validateForm();
};
// Watch for prop changes
watch(
() => props.initialAddress,
(newAddress) => {
formData.value.address = newAddress || "";
validateForm();
},
{ immediate: true },
);
watch(
() => props.visible,
(isVisible) => {
if (isVisible) {
resetForm();
}
},
);
// Initial validation
validateForm();
</script>
<style scoped>
.new-meeting-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-weight: 500;
color: #333;
font-size: 0.9em;
}
.required {
color: #e74c3c;
}
.address-input-group {
display: flex;
gap: 8px;
align-items: stretch;
}
.address-input {
flex: 1;
}
.search-btn {
flex-shrink: 0;
}
.address-search-results {
min-height: 200px;
}
.no-results {
text-align: center;
padding: 40px 20px;
color: #666;
}
.no-results i {
font-size: 2em;
color: #f39c12;
margin-bottom: 10px;
display: block;
}
.results-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.address-result-item {
padding: 12px 16px;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 12px;
}
.address-result-item:hover {
background-color: #f8f9fa;
border-color: #2196f3;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.address-result-item i {
color: #2196f3;
font-size: 1.1em;
}
.address-result-item span {
flex: 1;
font-size: 0.9em;
color: #333;
}
</style>

View file

@ -124,26 +124,38 @@ const columns = [
{
label: "Appt. Scheduled",
fieldName: "appointmentScheduledStatus",
type: "status",
type: "status-button",
sortable: true,
buttonVariant: "outlined",
onStatusClick: (status, rowData) => handleAppointmentClick(status, rowData),
disableCondition: (status) => status?.toLowerCase() !== "not started",
},
{
label: "Estimate Sent",
fieldName: "estimateSentStatus",
type: "status",
type: "status-button",
sortable: true,
buttonVariant: "outlined",
onStatusClick: (status, rowData) => handleEstimateClick(status, rowData),
disableCondition: (status) => status?.toLowerCase() !== "not started",
},
{
label: "Payment Received",
fieldName: "paymentReceivedStatus",
type: "status",
type: "status-button",
sortable: true,
buttonVariant: "outlined",
onStatusClick: (status, rowData) => handlePaymentClick(status, rowData),
disableCondition: (status) => status?.toLowerCase() !== "not started",
},
{
label: "Job Status",
fieldName: "jobStatus",
type: "status",
type: "status-button",
sortable: true,
buttonVariant: "outlined",
onStatusClick: (status, rowData) => handleJobClick(status, rowData),
disableCondition: (status) => status?.toLowerCase() !== "not started",
},
];
@ -335,6 +347,39 @@ const handleLazyLoad = async (event) => {
isLoading.value = false;
}
};
// Status button click handlers
const handleAppointmentClick = (status, rowData) => {
if (status?.toLowerCase() === "not started") {
// Navigate to schedule on-site meeting
const address = encodeURIComponent(rowData.address);
router.push(`/schedule-onsite?new=true&address=${address}`);
}
};
const handleEstimateClick = (status, rowData) => {
if (status?.toLowerCase() === "not started") {
// Navigate to create quotation/estimate
const address = encodeURIComponent(rowData.address);
router.push(`/quotations?new=true&address=${address}`);
}
};
const handlePaymentClick = (status, rowData) => {
if (status?.toLowerCase() === "not started") {
// Navigate to payment processing
const address = encodeURIComponent(rowData.address);
router.push(`/payments?new=true&address=${address}`);
}
};
const handleJobClick = (status, rowData) => {
if (status?.toLowerCase() === "not started") {
// Navigate to job creation
const address = encodeURIComponent(rowData.address);
router.push(`/jobs?new=true&address=${address}`);
}
};
// Watch for filters change to update status counts
watch(
() => filtersStore.getTableFilters("clients"),

File diff suppressed because it is too large Load diff