This commit is contained in:
Casey 2026-02-09 07:44:50 -06:00
parent c81beb5290
commit 38514fef47
24 changed files with 2087 additions and 322 deletions

View file

@ -70,8 +70,10 @@ const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_em
const FRAPPE_GET_WEEK_HOLIDAYS_METHOD = "custom_ui.api.db.general.get_week_holidays";
const FRAPPE_GET_DOC_LIST_METHOD = "custom_ui.api.db.general.get_doc_list";
// Service Appointment methods
const FRAPPE_GET_SERVICE_APPOINTMENT_METHOD = "custom_ui.api.db.service_appointments.get_service_appointment";
const FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD = "custom_ui.api.db.service_appointments.get_service_appointments";
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_scheduled_dates";
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_status";
class Api {
// ============================================================================
// CORE REQUEST METHOPD
@ -457,6 +459,10 @@ class Api {
// SERVICE APPOINTMENT METHODS
// ============================================================================
static async getServiceAppointment(serviceAppointmentName) {
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENT_METHOD, { serviceAppointmentName });
}
static async getServiceAppointments(companies = [], filters = {}) {
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { companies, filters });
}
@ -472,6 +478,10 @@ class Api {
})
}
static async setServiceAppointmentStatus(serviceAppointmentName, newStatus) {
return await this.request(FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD, { serviceAppointmentName, newStatus });
}
// ============================================================================
// TASK METHODS
// ============================================================================

View file

@ -1,6 +1,6 @@
<template>
<div class="calendar-navigation">
<Tabs value="0" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
<Tabs :value="defaultTab" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
<TabList>
<Tab value="0">Bids</Tab>
<Tab value="1">Projects</Tab>
@ -26,7 +26,7 @@
</TabPanel>
</TabPanels>
</Tabs>
<Tabs v-else value="0">
<Tabs v-else :value="defaultTab">
<TabList>
<Tab value="0">Bids</Tab>
<Tab value="1">Projects</Tab>
@ -61,20 +61,36 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import Tab from 'primevue/tab';
import Tabs from 'primevue/tabs';
import TabList from 'primevue/tablist';
import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels';
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
import JobsCalendar from '../calendar/jobs/JobsCalendar.vue';
import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue';
import { useNotificationStore } from '../../stores/notifications-primevue';
import { useCompanyStore } from '../../stores/company';
import { useRoute } from 'vue-router';
const notifications = useNotificationStore();
const companyStore = useCompanyStore();
const route = useRoute();
const defaultTab = ref('0');
const mappedTabs = {
'bid': '0',
'projects': '1',
'service': '2',
'warranties': companyStore.currentCompany == 'Sprinklers Northwest' ? '6' : '3'
};
onMounted(() => {
// Check for route query parameter to set default tab
if (route.query.tab) {
defaultTab.value = mappedTabs[route.query.tab] || '0';
}
});
</script>
<style scoped>

View file

@ -330,10 +330,14 @@ import { ref, onMounted, computed, watch } from "vue";
import Api from "../../../api";
import { useNotificationStore } from "../../../stores/notifications-primevue";
import { useCompanyStore } from "../../../stores/company";
import { useRoute } from "vue-router";
import JobDetailsModal from "../../modals/JobDetailsModal.vue";
const notifications = useNotificationStore();
const companyStore = useCompanyStore();
const route = useRoute();
const serviceAptToFind = route.query.apt || null;
// Reactive data
const scheduledServices = ref([]);
@ -1302,6 +1306,40 @@ const stopResize = async () => {
originalEndDate.value = null;
};
const handleServiceAptToFind = async () => {
if (!serviceAptToFind) return;
try {
// Fetch the specific service appointment
const appointment = await Api.getServiceAppointment(serviceAptToFind);
if (appointment && appointment.expectedStartDate) {
// Navigate to the week containing this appointment
weekStartDate.value = getWeekStart(parseLocalDate(appointment.expectedStartDate));
// Wait for data to load
await fetchServiceAppointments();
// Find the appointment in the loaded data
const foundAppointment = scheduledServices.value.find(s => s.name === serviceAptToFind) ||
unscheduledServices.value.find(s => s.name === serviceAptToFind);
if (foundAppointment) {
// Open the details modal
selectedEvent.value = foundAppointment;
eventDialog.value = true;
} else {
notifications.addWarning("Service appointment found but not visible in current filters");
}
} else {
notifications.addWarning("Service appointment not scheduled yet");
}
} catch (error) {
console.error("Error finding service appointment:", error);
notifications.addError("Failed to find service appointment");
}
};
const fetchServiceAppointments = async (currentDate) => {
try {
// Calculate date range for the week
@ -1397,6 +1435,9 @@ onMounted(async () => {
// await fetchProjects();
await fetchServiceAppointments();
await fetchHolidays();
// Handle serviceAptToFind query parameter
await handleServiceAptToFind();
});
</script>

View file

@ -187,6 +187,17 @@
<v-icon left>mdi-open-in-new</v-icon>
View Job
</v-btn>
<v-btn
v-if="job.status !== 'Completed'"
color="success"
variant="flat"
@click="updateStatus('Completed')"
:loading="isUpdatingStatus"
:disabled="isUpdatingStatus"
>
<v-icon left>mdi-check-circle</v-icon>
Mark as Completed
</v-btn>
<v-spacer></v-spacer>
<v-btn variant="text" @click="handleClose">Close</v-btn>
</v-card-actions>
@ -196,6 +207,10 @@
<script setup>
import { ref, computed } from "vue";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
const notifications = useNotificationStore();
// Props
const props = defineProps({
@ -214,7 +229,10 @@ const props = defineProps({
});
// Emits
const emit = defineEmits(["update:modelValue", "close"]);
const emit = defineEmits(["update:modelValue", "close", "statusUpdated"]);
// State
const isUpdatingStatus = ref(false);
// Computed
const showModal = computed({
@ -304,6 +322,34 @@ const getPriorityColor = (priority) => {
}
};
const updateStatus = async (newStatus) => {
if (!props.job?.name) return;
isUpdatingStatus.value = true;
try {
await Api.setServiceAppointmentStatus(props.job.name, newStatus);
notifications.addSuccess(`Service appointment marked as ${newStatus}`);
// Update local job status
if (props.job) {
props.job.status = newStatus;
}
// Emit event to parent to refresh data
emit("statusUpdated", { name: props.job.name, status: newStatus });
// Close modal after short delay
setTimeout(() => {
handleClose();
}, 1000);
} catch (error) {
console.error("Error updating service appointment status:", error);
notifications.addError("Failed to update service appointment status");
} finally {
isUpdatingStatus.value = false;
}
}
const viewJob = () => {
if (props.job?.name) {
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;

View file

@ -1163,6 +1163,8 @@ onMounted(async () => {
qty: item.qty,
rate: item.rate,
standardRate: item.rate,
bom: item.bom || null,
uom: item.uom || item.stockUom || item.stock_uom || 'Nos',
discountAmount: discountAmount === 0 ? null : discountAmount,
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
discountType: discountPercentage > 0 ? 'percentage' : 'currency'

File diff suppressed because it is too large Load diff

View file

@ -136,9 +136,9 @@ const chartData = ref({
})
const columns = [
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
{ label: "Overall Status", fieldName: "status", type: "status", sortable: true },
{ label: "Invoice Status", fieldName: "invoiceStatus", type: "text", sortable: true },
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
@ -275,7 +275,6 @@ const loadChartData = async () => {
// Load initial data
onMounted(async () => {
notifications.addWarning("Jobs page coming soon");
// Initialize pagination and filters
paginationStore.initializeTablePagination("jobs", { rows: 10 });
filtersStore.initializeTableFilters("jobs", columns);