updates for company effects
This commit is contained in:
parent
98ec082394
commit
7710a7c8fe
22 changed files with 941 additions and 186 deletions
|
|
@ -18,8 +18,10 @@ const FRAPPE_CREATE_ESTIMATE_TEMPLATE_METHOD = "custom_ui.api.db.estimates.creat
|
|||
const FRAPPE_GET_JOB_METHOD = "custom_ui.api.db.jobs.get_job";
|
||||
const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.jobs.get_jobs_table_data";
|
||||
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job";
|
||||
const FRAPPE_GET_JOB_TASK_LIST_METHOD = "custom_ui.api.db.jobs.get_job_task_table_data";
|
||||
const FRAPPE_GET_JOB_TASK_TABLE_DATA_METHOD = "custom_ui.api.db.jobs.get_job_task_table_data";
|
||||
const FRAPPE_GET_JOB_TASK_LIST_METHOD = "custom_ui.api.db.jobs.get_job_task_list";
|
||||
const FRAPPE_GET_INSTALL_PROJECTS_METHOD = "custom_ui.api.db.jobs.get_install_projects";
|
||||
const FRAPPE_GET_JOBS_FOR_CALENDAR_METHOD = "custom_ui.api.db.jobs.get_projects_for_calendar";
|
||||
const FRAPPE_GET_JOB_TEMPLATES_METHOD = "custom_ui.api.db.jobs.get_job_templates";
|
||||
// Task methods
|
||||
const FRAPPE_GET_TASKS_METHOD = "custom_ui.api.db.tasks.get_tasks_table_data";
|
||||
|
|
@ -42,6 +44,7 @@ const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses";
|
|||
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
|
||||
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts";
|
||||
const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_clients_table_data";
|
||||
const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2";
|
||||
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
||||
|
||||
|
|
@ -87,6 +90,17 @@ class Api {
|
|||
return await this.request(FRAPPE_GET_CLIENT_METHOD, { clientName });
|
||||
}
|
||||
|
||||
static async getPaginatedClientDetailsV2(paginationParams = {}, filters = {}, sortings = []) {
|
||||
const { page = 0, pageSize = 10 } = paginationParams;
|
||||
const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD, {
|
||||
filters,
|
||||
sortings,
|
||||
page: page + 1,
|
||||
pageSize,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated client data with filtering and sorting
|
||||
* @param {Object} paginationParams - Pagination parameters from store
|
||||
|
|
@ -138,9 +152,10 @@ class Api {
|
|||
// ON-SITE MEETING METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async getUnscheduledBidMeetings() {
|
||||
static async getUnscheduledBidMeetings(company) {
|
||||
return await this.request(
|
||||
"custom_ui.api.db.bid_meetings.get_unscheduled_bid_meetings",
|
||||
{ company }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +163,8 @@ class Api {
|
|||
return await this.request(FRAPPE_GET_ONSITE_MEETINGS_METHOD, { fields, filters });
|
||||
}
|
||||
|
||||
static async getWeekBidMeetings(weekStart, weekEnd) {
|
||||
return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd });
|
||||
static async getWeekBidMeetings(weekStart, weekEnd, company) {
|
||||
return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd, company });
|
||||
}
|
||||
|
||||
static async updateBidMeeting(name, data) {
|
||||
|
|
@ -300,6 +315,10 @@ class Api {
|
|||
return result;
|
||||
}
|
||||
|
||||
static async getJobsForCalendar(date, company = null, projectTemplates = []) {
|
||||
return await this.request(FRAPPE_GET_JOBS_FOR_CALENDAR_METHOD, { date, company, projectTemplates });
|
||||
}
|
||||
|
||||
static async getJob(jobName) {
|
||||
if (frappe.db.exists("Project", jobName)) {
|
||||
const result = await this.request(FRAPPE_GET_JOB_METHOD, { jobId: jobName })
|
||||
|
|
@ -346,7 +365,7 @@ class Api {
|
|||
|
||||
console.log("DEBUG: API - Sending job task options to backend:", options);
|
||||
|
||||
const result = await this.request(FRAPPE_GET_JOB_TASK_LIST_METHOD, { options, filters });
|
||||
const result = await this.request(FRAPPE_GET_JOB_TASK_TABLE_DATA_METHOD, { filters, sortings: sorting, page:page+1, pageSize });
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@
|
|||
<Tabs value="0">
|
||||
<TabList>
|
||||
<Tab value="0">Bids</Tab>
|
||||
<Tab value="1">Install</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
<Tab value="2">Service</Tab>
|
||||
<Tab value="3">Lowe Fencing</Tab>
|
||||
<Tab value="4">Daniel's Landscaping</Tab>
|
||||
<Tab value="5">Nuco Yardcare</Tab>
|
||||
<Tab value="6">Warranties</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
|
|
@ -22,21 +19,6 @@
|
|||
<p>Service feature coming soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Lowe Fencing" value="3">
|
||||
<div class="coming-soon">
|
||||
<p>Lowe Fencing calendar coming soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Daniel's Landscaping" value="4">
|
||||
<div class="coming-soon">
|
||||
<p>Daniel's Calendar coming soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Nuco Yardcare" value="5">
|
||||
<div class="coming-soon">
|
||||
<p>Nuco calendar coming soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Warranties" value="6">
|
||||
<div class="coming-soon">
|
||||
<p>Warranties Calendar coming soon!</p>
|
||||
|
|
@ -56,7 +38,7 @@ 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 InstallsCalendar from '../calendar/jobs/InstallsCalendar.vue';
|
||||
import InstallsCalendar from './jobs/ProjectsCalendar.vue';
|
||||
import { useNotificationStore } from '../../stores/notifications-primevue';
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
|
|
@ -65,6 +47,24 @@ const notifications = useNotificationStore();
|
|||
<style scoped>
|
||||
.calendar-navigation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-navigation :deep(.p-tabs) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-navigation :deep(.p-tabpanels) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-navigation :deep(.p-tabpanel) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
|
|
|
|||
|
|
@ -226,12 +226,14 @@ import BidMeetingModal from "../../modals/BidMeetingModal.vue";
|
|||
import MeetingDetailsModal from "../../modals/MeetingDetailsModal.vue";
|
||||
import { useLoadingStore } from "../../../stores/loading";
|
||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||
import { useCompanyStore } from "../../../stores/company";
|
||||
import Api from "../../../api";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loadingStore = useLoadingStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const companyStore = useCompanyStore();
|
||||
|
||||
// Query parameters
|
||||
const isNewMode = computed(() => route.query.new === "true");
|
||||
|
|
@ -816,7 +818,7 @@ const handleDropToUnscheduled = async (event) => {
|
|||
const loadUnscheduledMeetings = async () => {
|
||||
loadingStore.setLoading(true);
|
||||
try {
|
||||
const result = await Api.getUnscheduledBidMeetings();
|
||||
const result = await Api.getUnscheduledBidMeetings(companyStore.currentCompany);
|
||||
// Ensure we always have an array
|
||||
unscheduledMeetings.value = Array.isArray(result) ? result : [];
|
||||
console.log("Loaded unscheduled meetings:", unscheduledMeetings.value);
|
||||
|
|
@ -865,7 +867,7 @@ const loadWeekMeetings = async () => {
|
|||
|
||||
// Try to get meetings from API
|
||||
try {
|
||||
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr);
|
||||
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr, companyStore.currentCompany);
|
||||
if (Array.isArray(apiResult)) {
|
||||
// Transform the API data to match what the calendar expects
|
||||
meetings.value = apiResult
|
||||
|
|
@ -1085,6 +1087,15 @@ watch(currentWeekStart, () => {
|
|||
loadWeekMeetings();
|
||||
});
|
||||
|
||||
// Watch for company changes
|
||||
watch(
|
||||
() => companyStore.currentCompany,
|
||||
async () => {
|
||||
await loadWeekMeetings();
|
||||
await loadUnscheduledMeetings();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.new,
|
||||
(newVal) => {
|
||||
|
|
@ -1098,9 +1109,10 @@ watch(
|
|||
<style scoped>
|
||||
.schedule-bid-container {
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -1110,6 +1122,7 @@ watch(
|
|||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
|
|
@ -1150,9 +1163,9 @@ watch(
|
|||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
|
|
@ -1182,11 +1195,13 @@ watch(
|
|||
.sidebar-header h4 {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.unscheduled-meetings-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<h2>Daily Schedule - Installs</h2>
|
||||
<h2>Daily Schedule - {{ companyStore.currentCompany }}</h2>
|
||||
<div class="header-controls">
|
||||
<v-btn
|
||||
@click="previousDay"
|
||||
|
|
@ -26,6 +26,67 @@
|
|||
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
||||
>Today</v-btn
|
||||
>
|
||||
<v-menu
|
||||
v-model="showTemplateMenu"
|
||||
:close-on-content-click="false"
|
||||
location="bottom"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="ml-4"
|
||||
>
|
||||
<v-icon left size="small">mdi-file-document-multiple</v-icon>
|
||||
Project Template ({{ selectedProjectTemplates.length }})
|
||||
<v-icon right size="small">mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card min-width="300" max-width="400">
|
||||
<v-card-title class="text-subtitle-1 py-2">
|
||||
Select Project Templates
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class="pa-2">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="toggleAllTemplates">
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="selectedProjectTemplates.length === projectTemplates.length"
|
||||
:indeterminate="selectedProjectTemplates.length > 0 && selectedProjectTemplates.length < projectTemplates.length"
|
||||
></v-checkbox-btn>
|
||||
</template>
|
||||
<v-list-item-title>Select All</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item
|
||||
v-for="template in projectTemplates"
|
||||
:key="template.name"
|
||||
@click="toggleTemplate(template.name)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="selectedProjectTemplates.includes(template.name)"
|
||||
></v-checkbox-btn>
|
||||
</template>
|
||||
<v-list-item-title>{{ template.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="small"
|
||||
@click="applyTemplateFilter"
|
||||
>
|
||||
Apply
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-menu
|
||||
v-model="showForemenMenu"
|
||||
:close-on-content-click="false"
|
||||
|
|
@ -303,14 +364,41 @@
|
|||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import Api from "../../../api";
|
||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||
import { useCompanyStore } from "../../../stores/company";
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
// Reactive data
|
||||
const services = ref([]);
|
||||
const currentDate = ref(new Date().toISOString().split("T")[0]);
|
||||
const eventDialog = ref(false);
|
||||
const selectedEvent = ref(null);
|
||||
const projectTemplates = ref([]);
|
||||
|
||||
// Helper function to get crew name from foreman ID
|
||||
const getCrewNameFromForemanId = (foremanId) => {
|
||||
if (!foremanId) return null;
|
||||
// If it's already a crew name format, return it
|
||||
if (foremanId.startsWith('Crew ')) return foremanId;
|
||||
// For now, we'll need to map the foreman employee ID to crew names
|
||||
// This could be enhanced with an API call to get foreman details
|
||||
return foremanId; // Return as-is for now
|
||||
};
|
||||
|
||||
// Helper function to calculate duration in minutes from start and end times
|
||||
const calculateDuration = (startTime, endTime) => {
|
||||
if (!startTime || !endTime) return 120; // Default 2 hours
|
||||
|
||||
const [startHours, startMinutes] = startTime.split(':').map(Number);
|
||||
const [endHours, endMinutes] = endTime.split(':').map(Number);
|
||||
|
||||
const startTotalMinutes = startHours * 60 + startMinutes;
|
||||
const endTotalMinutes = endHours * 60 + endMinutes;
|
||||
|
||||
return endTotalMinutes - startTotalMinutes;
|
||||
};
|
||||
|
||||
// Drag and drop state
|
||||
const draggedService = ref(null);
|
||||
|
|
@ -340,14 +428,10 @@ const showForemenMenu = ref(false);
|
|||
const showDatePicker = ref(false);
|
||||
const selectedDate = ref(null);
|
||||
|
||||
// Computed properties
|
||||
const scheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "scheduled" && service.foreman),
|
||||
);
|
||||
// Project template filter
|
||||
const selectedProjectTemplates = ref([]);
|
||||
const showTemplateMenu = ref(false);
|
||||
|
||||
const unscheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "unscheduled" || !service.foreman),
|
||||
);
|
||||
|
||||
// Daily calendar computed properties
|
||||
const dayDisplayText = computed(() => {
|
||||
|
|
@ -394,6 +478,15 @@ const timeSlots = computed(() => {
|
|||
return slots;
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const scheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "scheduled" && service.foreman),
|
||||
);
|
||||
|
||||
const unscheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "unscheduled" || !service.foreman),
|
||||
);
|
||||
|
||||
// Methods
|
||||
const getUnscheduledCount = () => unscheduledServices.value.length;
|
||||
|
||||
|
|
@ -551,6 +644,31 @@ const toggleForeman = (foremanId) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Project template selection methods
|
||||
const toggleAllTemplates = () => {
|
||||
if (selectedProjectTemplates.value.length === projectTemplates.value.length) {
|
||||
// Deselect all
|
||||
selectedProjectTemplates.value = [];
|
||||
} else {
|
||||
// Select all
|
||||
selectedProjectTemplates.value = projectTemplates.value.map(t => t.name);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTemplate = (templateName) => {
|
||||
const index = selectedProjectTemplates.value.indexOf(templateName);
|
||||
if (index > -1) {
|
||||
selectedProjectTemplates.value.splice(index, 1);
|
||||
} else {
|
||||
selectedProjectTemplates.value.push(templateName);
|
||||
}
|
||||
};
|
||||
|
||||
const applyTemplateFilter = async () => {
|
||||
showTemplateMenu.value = false;
|
||||
await fetchProjects(currentDate.value);
|
||||
};
|
||||
|
||||
// Date picker methods
|
||||
const onDateSelected = (date) => {
|
||||
if (date) {
|
||||
|
|
@ -742,10 +860,12 @@ const handleDrop = async (event, foremanId, time) => {
|
|||
try {
|
||||
await Api.upsertJob({
|
||||
id: draggedService.value.id,
|
||||
scheduledDate: currentDate.value,
|
||||
foreman: foreman.name
|
||||
expectedStartDate: currentDate.value,
|
||||
expectedStartTime: time,
|
||||
customForeman: foreman.name // This should ideally be the foreman employee ID
|
||||
});
|
||||
notifications.addSuccess("Job scheduled successfully");
|
||||
notifications.addWarning("This feature is currently still in development. This job has not actually been scheduled yet.");
|
||||
} catch (error) {
|
||||
console.error("Error scheduling job:", error);
|
||||
notifications.addError("Failed to schedule job");
|
||||
|
|
@ -793,8 +913,9 @@ const handleUnscheduledDrop = async (event) => {
|
|||
try {
|
||||
await Api.upsertJob({
|
||||
id: draggedService.value.id,
|
||||
scheduledDate: null,
|
||||
foreman: null
|
||||
expectedStartDate: null,
|
||||
expectedStartTime: null,
|
||||
customForeman: null
|
||||
});
|
||||
notifications.addSuccess("Job unscheduled successfully");
|
||||
} catch (error) {
|
||||
|
|
@ -812,31 +933,116 @@ const handleUnscheduledDrop = async (event) => {
|
|||
|
||||
const fetchProjects = async (date) => {
|
||||
try {
|
||||
const data = await Api.getInstallProjects(date, date);
|
||||
services.value = data;
|
||||
console.log("Loaded install projects:", data);
|
||||
const data = await Api.getJobsForCalendar(date, companyStore.currentCompany, selectedProjectTemplates.value);
|
||||
|
||||
// Transform the API response into the format the component expects
|
||||
const transformedServices = [];
|
||||
|
||||
// Process scheduled projects
|
||||
if (data.projects && Array.isArray(data.projects)) {
|
||||
data.projects.forEach(project => {
|
||||
const crewName = getCrewNameFromForemanId(project.customForeman);
|
||||
const duration = calculateDuration(project.expectedStartTime, project.expectedEndTime);
|
||||
|
||||
transformedServices.push({
|
||||
id: project.name,
|
||||
title: project.projectName || project.jobAddress || 'Unnamed Project',
|
||||
serviceType: project.projectName || project.jobAddress || 'Install Project',
|
||||
customer: project.customer || 'Unknown Customer',
|
||||
address: project.jobAddress || project.customInstallationAddress || '',
|
||||
scheduledDate: project.expectedStartDate || date,
|
||||
scheduledTime: project.expectedStartTime || '08:00',
|
||||
duration: duration,
|
||||
foreman: crewName,
|
||||
priority: (project.priority || 'Medium').toLowerCase(),
|
||||
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
|
||||
notes: project.notes || '',
|
||||
status: 'scheduled'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process unscheduled projects
|
||||
if (data.unscheduledProjects && Array.isArray(data.unscheduledProjects)) {
|
||||
data.unscheduledProjects.forEach(project => {
|
||||
const duration = calculateDuration(project.expectedStartTime, project.expectedEndTime);
|
||||
|
||||
transformedServices.push({
|
||||
id: project.name,
|
||||
title: project.projectName || project.jobAddress || 'Unnamed Project',
|
||||
serviceType: project.projectName || project.jobAddress || 'Install Project',
|
||||
customer: project.customer || 'Unknown Customer',
|
||||
address: project.jobAddress || project.customInstallationAddress || '',
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: duration,
|
||||
foreman: null,
|
||||
priority: (project.priority || 'Medium').toLowerCase(),
|
||||
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
|
||||
notes: project.notes || '',
|
||||
status: 'unscheduled'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
services.value = transformedServices;
|
||||
console.log("Loaded install projects:", transformedServices);
|
||||
} catch (error) {
|
||||
console.error("Error loading install projects:", error);
|
||||
notifications.addError("Failed to load install projects");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchForemen = async () => {
|
||||
// try {
|
||||
// const data = await Api.getForemen(companyStore.currentCompany);
|
||||
// foremen.value = data;
|
||||
// console.log("Loaded foremen:", data);
|
||||
// } catch (error) {
|
||||
// console.error("Error loading foremen:", error);
|
||||
// notifications.addError("Failed to load foremen");
|
||||
// }
|
||||
};
|
||||
|
||||
const fetchProjectTemplates = async () => {
|
||||
try {
|
||||
const data = await Api.getJobTemplates(companyStore.currentCompany);
|
||||
projectTemplates.value = data;
|
||||
// Select all templates by default
|
||||
selectedProjectTemplates.value = data.map(t => t.name);
|
||||
console.log("Loaded project templates:", data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error loading project templates:", error);
|
||||
notifications.addError("Failed to load project templates");
|
||||
}
|
||||
}
|
||||
|
||||
watch(currentDate, async (newDate) => {
|
||||
await fetchProjects(newDate);
|
||||
});
|
||||
|
||||
watch(companyStore, async (newCompany) => {
|
||||
await fetchForemen();
|
||||
await fetchProjectTemplates();
|
||||
await fetchProjects(currentDate.value);
|
||||
}, { deep: true });
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await fetchProjects(currentDate.value);
|
||||
await fetchForemen();
|
||||
await fetchProjectTemplates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-container {
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
|
|
@ -846,6 +1052,7 @@ onMounted(async () => {
|
|||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
|
|
@ -1071,6 +1278,8 @@ onMounted(async () => {
|
|||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.unscheduled-header {
|
||||
|
|
@ -1078,6 +1287,7 @@ onMounted(async () => {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.unscheduled-header h4 {
|
||||
|
|
@ -1088,7 +1298,7 @@ onMounted(async () => {
|
|||
.unscheduled-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-x: hidden;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
</template>
|
||||
|
||||
<!-- SNW Installation Status -->
|
||||
<div v-if="!isNew && !editMode" class="install-status-section">
|
||||
<div v-if="!isNew && !editMode && isSNWClient" class="install-status-section">
|
||||
<InstallStatus
|
||||
:onsite-meeting-status="snwInstallData.onsiteMeetingStatus"
|
||||
:estimate-sent-status="snwInstallData.estimateSentStatus"
|
||||
|
|
@ -438,6 +438,13 @@ const fullAddress = computed(() => {
|
|||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||
});
|
||||
|
||||
// Check if client is associated with Sprinklers Northwest
|
||||
const isSNWClient = computed(() => {
|
||||
// if (!props.clientData?.companies || !Array.isArray(props.clientData.companies)) return false;
|
||||
// return props.clientData.companies.some((c) => c.company === "Sprinklers Northwest");
|
||||
return companyStore.currentCompany === "Sprinklers Northwest";
|
||||
});
|
||||
|
||||
// Computed data for SNW Install status
|
||||
const snwInstallData = computed(() => {
|
||||
if (!selectedAddressData.value) {
|
||||
|
|
|
|||
|
|
@ -783,7 +783,9 @@ const hasExactlyOneRowSelected = computed(() => {
|
|||
onMounted(() => {
|
||||
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||
filterableColumns.value.forEach((col) => {
|
||||
pendingFilters.value[col.fieldName] = currentFilters[col.fieldName]?.value || "";
|
||||
// Use defaultValue from column definition if provided, otherwise use stored filter value
|
||||
const storedValue = currentFilters[col.fieldName]?.value || "";
|
||||
pendingFilters.value[col.fieldName] = col.defaultValue || storedValue;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-[9999] transition-opacity duration-200"
|
||||
:class="{ 'opacity-100': showOverlay, 'opacity-0 pointer-events-none': !showOverlay }"
|
||||
class="global-loading-overlay"
|
||||
>
|
||||
<div class="bg-white rounded-lg p-6 shadow-xl max-w-sm w-full mx-4 text-center">
|
||||
<div class="mb-4">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-blue-500"></i>
|
||||
<div class="loading-content">
|
||||
<div class="spinner-container">
|
||||
<i class="pi pi-spin pi-spinner"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">Loading</h3>
|
||||
<p class="text-gray-600">{{ loadingMessage }}</p>
|
||||
<h3 class="loading-title">Loading</h3>
|
||||
<p class="loading-message">{{ loadingMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -46,15 +45,51 @@ const loadingMessage = computed(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styling for better visual appearance */
|
||||
.bg-opacity-30 {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
.global-loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* Backdrop blur effect for modern browsers */
|
||||
@supports (backdrop-filter: blur(4px)) {
|
||||
.fixed.inset-0 {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.loading-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.spinner-container i {
|
||||
font-size: 3rem;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,4 +7,7 @@ import CalendarNavigation from '@/components/calendar/CalendarNavigation.vue'
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.calendar-navigation) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ import { useFiltersStore } from "../../stores/filters";
|
|||
import { useModalStore } from "../../stores/modal";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
import { useCompanyStore } from "../../stores/company";
|
||||
import TodoChart from "../common/TodoChart.vue";
|
||||
import { Calendar, Community, Hammer, PathArrowSolid, Clock, Shield, ShieldSearch,
|
||||
ClipboardCheck, DoubleCheck, CreditCard, CardNoAccess, ChatBubbleQuestion, Edit,
|
||||
WateringSoil, Soil, Truck, SoilAlt } from "@iconoir/vue";
|
||||
WateringSoil, Soil, Truck, SoilAlt,
|
||||
Filter} from "@iconoir/vue";
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
|
|
@ -41,6 +43,7 @@ const filtersStore = useFiltersStore();
|
|||
const modalStore = useModalStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const companyStore = useCompanyStore();
|
||||
|
||||
const tableData = ref([]);
|
||||
const totalRecords = ref(0);
|
||||
|
|
@ -50,10 +53,26 @@ const currentWeekParams = ref({});
|
|||
const chartLoading = ref(true); // Start with loading state
|
||||
|
||||
const lookup = route.query.lookup;
|
||||
const lastLazyLoadEvent = ref(null);
|
||||
|
||||
// Watch for company changes to reload data
|
||||
watch(
|
||||
() => companyStore.currentCompany,
|
||||
async () => {
|
||||
console.log("Company changed, reloading client data...");
|
||||
if (lastLazyLoadEvent.value) {
|
||||
await handleLazyLoad(lastLazyLoadEvent.value);
|
||||
}
|
||||
// Also refresh status counts
|
||||
await refreshStatusCounts();
|
||||
}
|
||||
);
|
||||
|
||||
// Computed property to get current filters for the chart
|
||||
const currentFilters = computed(() => {
|
||||
return filtersStore.getTableFilters("clients");
|
||||
filters = { ...filtersStore.getTableFilters("clients"),
|
||||
company: { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS}
|
||||
};
|
||||
});
|
||||
|
||||
// Handle week change from chart
|
||||
|
|
@ -220,6 +239,7 @@ const tableActions = [
|
|||
// Handle lazy loading events from DataTable
|
||||
const handleLazyLoad = async (event) => {
|
||||
console.log("Clients page - handling lazy load:", event);
|
||||
lastLazyLoadEvent.value = event;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
|
@ -263,8 +283,9 @@ const handleLazyLoad = async (event) => {
|
|||
filters,
|
||||
sortingArray,
|
||||
});
|
||||
filters["company"] = { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS};
|
||||
|
||||
const result = await Api.getPaginatedClientDetails(
|
||||
const result = await Api.getPaginatedClientDetailsV2(
|
||||
paginationParams,
|
||||
filters,
|
||||
sortingArray,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const notifications = useNotificationStore();
|
|||
|
||||
const route = useRoute();
|
||||
|
||||
const jobIdQuery = computed(() => route.query.jobId || "");
|
||||
const jobIdQuery = computed(() => route.query.name || "");
|
||||
const isNew = computed(() => route.query.new === "true");
|
||||
|
||||
const tableData = ref([]);
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ const handleLazyLoad = async (event) => {
|
|||
|
||||
const handleRowClick = (event) => {
|
||||
const rowData = event.data;
|
||||
router.push(`/job?jobId=${rowData.name}`);
|
||||
router.push(`/job?name=${rowData.name}`);
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const currentFilters = computed(() => {
|
|||
|
||||
const columns = [
|
||||
{ label: "Task", fieldName: "subject", type: "text", sortable: true, filterable: true,
|
||||
filterInputID: "subjectFilterId" },
|
||||
filterInputID: "subjectFilterId", defaultValue: subject || null },
|
||||
{ label: "Job", fieldName: "project", type: "link", sortable: true,
|
||||
onLinkClick: (link, rowData) => handleProjectClick(link, rowData)
|
||||
},
|
||||
|
|
@ -220,11 +220,6 @@ watch(showCompleted, () => {
|
|||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
if (subject) {
|
||||
const inputElement = document.getElementById(`filter-subject`);
|
||||
inputElement.text = subject;
|
||||
}
|
||||
notifications.addWarning("Tasks page coming soon");
|
||||
// Initialize pagination and filters
|
||||
paginationStore.initializeTablePagination("tasks", { rows: 10 });
|
||||
filtersStore.initializeTableFilters("tasks", columns);
|
||||
|
|
@ -236,11 +231,8 @@ onMounted(async () => {
|
|||
const initialSorting = filtersStore.getTableSorting("tasks");
|
||||
|
||||
if (subject) {
|
||||
console.log(subject);
|
||||
console.log(initialFilters);
|
||||
console.log(initialFilters.subject);
|
||||
console.log("Setting subject filter from query param:", subject);
|
||||
initialFilters.subject.value = subject;
|
||||
//initialFilters = {...initialFilters, subject: {value: subject, match_mode: "contains"}};
|
||||
}
|
||||
|
||||
const optionsResult = await Api.getTaskStatusOptions();
|
||||
|
|
@ -255,6 +247,8 @@ onMounted(async () => {
|
|||
sortOrder: initialSorting.order || initialPagination.sortOrder,
|
||||
filters: initialFilters,
|
||||
});
|
||||
|
||||
notifications.addWarning("Tasks page coming soon");
|
||||
});
|
||||
</script>
|
||||
<style lang="css">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue