This commit is contained in:
Casey 2026-02-15 08:00:18 -06:00
parent 8c818f8dde
commit e2746b83bb
2 changed files with 412 additions and 383 deletions

View File

@ -6,10 +6,12 @@ import frappe
from .utils import create_module
import holidays
from datetime import date, timedelta
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
def after_install():
create_module()
add_custom_fields()
# add_custom_fields()
frappe.db.commit()
# Proper way to refresh metadata
@ -31,7 +33,7 @@ def after_install():
def after_migrate():
add_custom_fields()
update_onsite_meeting_fields()
# update_onsite_meeting_fields()
frappe.db.commit()
# Proper way to refresh metadata for all doctypes with custom fields
@ -40,13 +42,13 @@ def after_migrate():
frappe.clear_cache(doctype=doctype)
frappe.reload_doctype(doctype)
check_and_create_holiday_list()
# check_and_create_holiday_list()
# create_project_templates()
# create_task_types()
# create_tasks()
create_bid_meeting_note_form_templates()
# create_bid_meeting_note_form_templates()
create_accounts()
create_companies()
# create_companies()
# init_stripe_accounts()
# update_address_fields()
@ -1592,126 +1594,24 @@ def create_bid_meeting_note_form_templates():
doc.insert(ignore_permissions=True)
def create_companies():
def create_accounts():
"""Create necessary companies if they do not exist."""
print("\n🔧 Checking for necessary companies...")
companies = [
{
'company_name': 'Veritas Stone',
'abbr': 'VS',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Cash - VS',
'default_receivable_account': 'Debtors - VS',
'default_payable_account': 'Creditors - VS',
'default_income_account': 'Sales - VS',
'default_expense_account': 'Cost of Goods Sold - VS',
'cost_center': 'Main - VS',
'enable_perpetual_inventory': 1
},
{
'company_name': 'Daniels Landscape Supplies',
'abbr': 'DL',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Cash - DL',
'default_receivable_account': 'Debtors - DL',
'default_payable_account': 'Creditors - DL',
'default_income_account': 'Sales - DL',
'default_expense_account': 'Cost of Goods Sold - DL',
'cost_center': 'Main - DL',
'enable_perpetual_inventory': 1
},
{
'company_name': 'sprinklersnorthwest (Demo)',
'abbr': 'SD',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Cash - SD',
'default_receivable_account': 'Debtors - SD',
'default_payable_account': 'Creditors - SD',
'default_income_account': 'Sales - SD',
'default_expense_account': 'Cost of Goods Sold - SD',
'cost_center': 'Main - SD',
'enable_perpetual_inventory': 1
},
{
'company_name': 'Lowe Fencing',
'abbr': 'LF',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Cash - LF',
'default_receivable_account': 'Debtors - LF',
'default_payable_account': 'Creditors - LF',
'default_income_account': 'Fencing Sales - LF',
'default_expense_account': 'Cost of Goods Sold - LF',
'cost_center': 'Main - LF',
'enable_perpetual_inventory': 1
},
{
'company_name': 'Nuco Yard Care',
'abbr': 'NYC',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Cash - NYC',
'default_receivable_account': 'Debtors - NYC',
'default_payable_account': 'Creditors - NYC',
'default_income_account': 'Sales - NYC',
'default_expense_account': 'Cost of Goods Sold - NYC',
'cost_center': 'Main - NYC',
'enable_perpetual_inventory': 1
},
{
'company_name': 'Sprinklers Northwest',
'abbr': 'S',
'default_currency': 'USD',
'country': 'United States',
'is_group': 0,
'parent_company': None,
'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': 'Standard',
'default_cash_account': 'Undeposited Funds - S',
'default_receivable_account': 'Debtors - S',
'default_payable_account': 'Creditors - S',
'default_income_account': 'Sales - S',
'default_expense_account': 'Cost of Goods Sold - S',
'cost_center': 'Main - S',
'enable_perpetual_inventory': 1
}
]
companies = frappe.get_all("Company", pluck="name")
for company in companies:
if frappe.db.exists("Company", company["company_name"]):
if frappe.db.exists("Account", {"company": company}):
print(f"✅ Accounts already exist for company '{company}'. Skipping account creation.")
continue
data = {
"doctype": "Company"
}
data.update(company)
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)
company_doc = frappe.get_doc("Company", company)
create_charts(
company=company.name,
chart_template=company_doc.chart_template
)
def create_accounts():
def create_stripe_accounts():
"""Create necessary accounts if they do not exist."""
print("\n🔧 Checking for necessary accounts...")
@ -1745,23 +1645,3 @@ def create_accounts():
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
frappe.db.commit()
def init_stripe_accounts():
"""Initializes the bare configurations for each Stripe Settings doctypes."""
print("\n🔧 Initializing Stripe Settings for companies...")
companies = ["Sprinklers Northwest"]
for company in companies:
if not frappe.db.exists("Stripe Settings", {"company": company}):
doc = frappe.get_doc({
"doctype": "Stripe Settings",
"company": company,
"api_key": "",
"publishable_key": "",
"webhook_secret": "",
"account": f"Stripe Clearing - {company}"
})
doc.insert(ignore_permissions=True)
frappe.db.commit()

View File

@ -168,17 +168,32 @@
</v-card>
</v-dialog>
<!-- Holiday Prompt Dialog -->
<v-dialog v-model="showHolidayPrompt" max-width="500px">
<!-- Skip Day Confirmation Dialog -->
<v-dialog v-model="showSkipConfirmation" max-width="400px">
<v-card>
<v-card-title>Holiday Scheduling</v-card-title>
<v-card-title>Skip Day Confirmation</v-card-title>
<v-card-text>
The event has been scheduled on or over a holiday/Sunday. Should this day be skipped?
<p>Are you sure you want to skip <strong>{{ skipConfirmationData ? formatDate(skipConfirmationData.date) : '' }}</strong> for job <strong>{{ skipConfirmationData ? skipConfirmationData.job.projectTemplate : '' }}</strong>?</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="handleHolidayChoice(false)">Include Holiday</v-btn>
<v-btn color="primary" @click="handleHolidayChoice(true)">Skip Holiday</v-btn>
<v-btn @click="cancelSkipDay">Cancel</v-btn>
<v-btn color="error" @click="confirmSkipDay">Skip Day</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Remove Skip Confirmation Dialog -->
<v-dialog v-model="showRemoveSkipConfirmation" max-width="400px">
<v-card>
<v-card-title>Remove Skip Confirmation</v-card-title>
<v-card-text>
<p>Are you sure you want to remove the skip for <strong>{{ removeSkipConfirmationData ? formatDate(removeSkipConfirmationData.date) : '' }}</strong> on job <strong>{{ removeSkipConfirmationData ? removeSkipConfirmationData.job.projectTemplate : '' }}</strong>?</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancelRemoveSkip">Cancel</v-btn>
<v-btn color="error" @click="confirmRemoveSkip">Remove Skip</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@ -220,6 +235,7 @@
'holiday': isHoliday(day.date),
'sunday': isSunday(day.date),
'drag-over': isDragOver && dragOverCell?.foremanId === foreman.name && dragOverCell?.date === day.date,
'has-skipped-jobs': getSkippedJobsForCell(foreman.name, day.date).length > 0,
}"
@dragover="handleDragOver($event, foreman.name, day.date)"
@dragleave="handleDragLeave"
@ -233,7 +249,7 @@
class="calendar-job"
:style="getJobStyle(job, day.date)"
:draggable="job.status === 'Scheduled'"
@click.stop="showEventDetails({ event: job })"
@click.stop="skipMode ? handleSkipDayClick(foreman.name, day.date, job) : showEventDetails({ event: job })"
@dragstart="job.status === 'Scheduled' ? handleDragStart(job, $event) : null"
@dragend="handleDragEnd"
@mousedown="(job.status === 'Scheduled' || job.status === 'Started') ? startResize($event, job, day.date) : null"
@ -256,6 +272,7 @@
class="skipped-day"
:class="getPriorityClass(job.priority)"
>
<span>Skipped</span>
<button
class="remove-skip-btn"
@click.stop="handleRemoveSkip(job, day.date)"
@ -264,6 +281,8 @@
<v-icon size="small">mdi-close</v-icon>
</button>
</div>
<!-- Holiday connector line for split jobs -->
<template v-if="isHoliday(day.date)">
<div
v-for="job in getJobsWithConnector(foreman.name, day.date)"
@ -376,9 +395,23 @@ import JobDetailsModal from "../../modals/JobDetailsModal.vue";
const notifications = useNotificationStore();
const companyStore = useCompanyStore();
const route = useRoute();
const random = 3
const serviceAptToFind = route.query.apt || null;
// Helper function to get all holidays in a date range
function getHolidaysInRange(startDate, endDate) {
const start = parseLocalDate(startDate);
const end = parseLocalDate(endDate);
const holidaysInRange = [];
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = toLocalDateString(d);
if (isHoliday(dateStr)) {
holidaysInRange.push(dateStr);
}
}
return holidaysInRange;
}
// Reactive data
const scheduledServices = ref([]);
const unscheduledServices = ref([]);
@ -403,6 +436,9 @@ const resizeStartDate = ref(null);
const originalEndDate = ref(null);
const justFinishedResize = ref(false);
// Skip mode
const skipMode = ref(false);
// Foremen data (Crews)
const foremen = ref([]);
@ -414,13 +450,13 @@ const showForemenMenu = ref(false);
const showDatePicker = ref(false);
const selectedDate = ref(null);
// Holiday prompt
const showHolidayPrompt = ref(false);
const pendingAction = ref(null); // { type: 'drop' or 'resize', data: {...} }
const holidayDates = ref([]); // dates that are holidays in the range
// Skip day confirmation
const showSkipConfirmation = ref(false);
const skipConfirmationData = ref(null);
// Skip mode
const skipMode = ref(false);
// Remove skip confirmation
const showRemoveSkipConfirmation = ref(false);
const removeSkipConfirmationData = ref(null);
// Project template filter
const selectedProjectTemplates = ref([]);
@ -561,21 +597,7 @@ function getCrewName(foremanId) {
return foreman.customCrew ? `${foreman.employeeName} (Crew ${foreman.customCrew})` : foreman.employeeName;
}
// Helper function to get all Sundays in a date range
function getSundaysInRange(startDate, endDate) {
const start = parseLocalDate(startDate);
const end = parseLocalDate(endDate);
const sundays = [];
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = toLocalDateString(d);
if (isSunday(dateStr)) {
sundays.push(dateStr);
}
}
return sundays;
}
// Helper function to calculate job segments (parts between holidays/Sundays/skipDays)
// Helper function to calculate job segments (parts between holidays/Sundays)
function getJobSegments(job) {
const startDate = job.expectedStartDate;
const endDate = job.expectedEndDate || job.expectedStartDate;
@ -827,108 +849,6 @@ const toggleSkipMode = () => {
skipMode.value = !skipMode.value;
};
const handleSkipDayClick = async (foremanId, date) => {
// Find jobs that cover this date for this foreman
const jobs = scheduledServices.value.filter(job => {
if (job.foreman !== foremanId) return false;
const start = job.expectedStartDate;
const end = job.expectedEndDate || start;
return date >= start && date <= end;
});
if (jobs.length === 0) {
notifications.addInfo("No jobs found for this day.");
return;
}
// For now, take the first job. In future, could show selection if multiple.
const job = jobs[0];
// Check if already skipped
const isAlreadySkipped = job.skipDays && job.skipDays.some(skip => skip.date === date);
if (isAlreadySkipped) {
notifications.addInfo("This day is already skipped.");
return;
}
// Prompt
const confirmed = confirm(`Are you sure you want to skip ${formatDate(date)} for job ${job.projectTemplate}?`);
if (!confirmed) return;
// Add to skipDays
const newSkipDays = [...(job.skipDays || []), { date }];
// Update local
const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: newSkipDays
};
}
// Call API
try {
await Api.updateServiceAppointmentScheduledDates(
job.name,
job.expectedStartDate,
job.expectedEndDate,
job.foreman,
newSkipDays
);
notifications.addSuccess("Day skipped successfully!");
} catch (error) {
console.error("Error skipping day:", error);
notifications.addError("Failed to skip day");
// Revert
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: job.skipDays
};
}
}
};
// Remove skip day
const handleRemoveSkip = async (job, date) => {
const confirmed = confirm(`Remove skipped day ${formatDate(date)} for job ${job.projectTemplate}?`);
if (!confirmed) return;
const newSkipDays = (job.skipDays || []).filter(skip => skip.date !== date);
// Update local
const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: newSkipDays
};
}
// Call API
try {
await Api.updateServiceAppointmentScheduledDates(
job.name,
job.expectedStartDate,
job.expectedEndDate,
job.foreman,
newSkipDays
);
notifications.addSuccess("Skipped day removed successfully!");
} catch (error) {
console.error("Error removing skipped day:", error);
notifications.addError("Failed to remove skipped day");
// Revert
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: job.skipDays
};
}
}
};
// Foremen selection methods
const toggleAllForemen = () => {
if (selectedForemen.value.length === foremen.value.length) {
@ -1095,6 +1015,25 @@ const handleDrop = async (event, foremanId, date) => {
if (!draggedService.value) return;
// Prevent dropping on Sunday
if (isSunday(date)) {
notifications.addError("Cannot schedule jobs on Sunday. Please select a weekday.");
isDragOver.value = false;
dragOverCell.value = null;
draggedService.value = null;
return;
}
// Prevent dropping on holidays
if (isHoliday(date)) {
const holidayDesc = getHolidayDescription(date);
notifications.addError(`Cannot schedule jobs on ${holidayDesc || 'a holiday'}. Please select a different date.`);
isDragOver.value = false;
dragOverCell.value = null;
draggedService.value = null;
return;
}
// Get foreman details
const foreman = foremen.value.find(f => f.name === foremanId);
if (!foreman) return;
@ -1102,29 +1041,16 @@ const handleDrop = async (event, foremanId, date) => {
// Default to single day
let endDate = date;
// Check for holidays or Sundays in the range
const holidaysInRange = getHolidaysInRange(date, endDate);
const sundaysInRange = getSundaysInRange(date, endDate);
const conflictingDates = [...holidaysInRange, ...sundaysInRange];
if (conflictingDates.length > 0) {
// Show prompt
holidayDates.value = conflictingDates;
pendingAction.value = {
type: 'drop',
data: { event, foremanId, date, endDate, foreman }
};
showHolidayPrompt.value = true;
// Check for holidays in the range
if (hasHolidayInRange(date, endDate)) {
notifications.addError("Cannot schedule job on a holiday. Please select different dates.");
// Reset drag state
isDragOver.value = false;
dragOverCell.value = null;
draggedService.value = null;
return;
}
// Proceed with normal drop
await performDrop(foremanId, date, endDate, foreman);
};
const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) => {
// Check if this is scheduling an unscheduled job or moving a scheduled job
const unscheduledIndex = unscheduledServices.value.findIndex(s => s.name === draggedService.value.name);
const scheduledIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
@ -1140,8 +1066,7 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
draggedService.value.name,
date,
endDate,
foreman.name,
skipDays
foreman.name
);
// Remove from unscheduled and add to scheduled
const scheduledService = {
@ -1149,8 +1074,7 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
expectedStartDate: date,
expectedEndDate: endDate,
foreman: foreman.name,
status: 'Scheduled',
skipDays: skipDays
status: 'Scheduled'
};
unscheduledServices.value.splice(unscheduledIndex, 1);
scheduledServices.value.push(scheduledService);
@ -1169,16 +1093,14 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
draggedService.value.name,
date,
date, // Reset to single day when moved
foreman.name,
skipDays
foreman.name
);
// Update the scheduled job
scheduledServices.value[scheduledIndex] = {
...scheduledServices.value[scheduledIndex],
expectedStartDate: date,
expectedEndDate: date, // Reset to single day
foreman: foreman.name,
skipDays: skipDays
foreman: foreman.name
};
notifications.addSuccess("Job moved successfully!");
} catch (error) {
@ -1193,58 +1115,150 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
draggedService.value = null;
};
const handleHolidayChoice = async (skip) => {
showHolidayPrompt.value = false;
const action = pendingAction.value;
if (!action) return;
const skipDays = skip ? holidayDates.value : [];
if (action.type === 'drop') {
const { foremanId, date, endDate, foreman } = action.data;
await performDrop(foremanId, date, endDate, foreman, skipDays);
} else if (action.type === 'resize') {
await performResize(action.data, skipDays);
const handleSkipDayClick = async (foremanId, date, specificJob = null) => {
let job;
if (specificJob) {
job = specificJob;
} else {
// Find jobs that cover this date for this foreman
const jobs = scheduledServices.value.filter(job => {
if (job.foreman !== foremanId) return false;
const start = job.expectedStartDate;
const end = job.expectedEndDate || start;
return date >= start && date <= end;
});
if (jobs.length === 0) {
notifications.addInfo("No jobs found for this day.");
return;
}
// For now, take the first job. In future, could show selection if multiple.
job = jobs[0];
}
pendingAction.value = null;
holidayDates.value = [];
// Check if already skipped
const isAlreadySkipped = job.skipDays && job.skipDays.some(skip => skip.date === date);
if (isAlreadySkipped) {
notifications.addInfo("This day is already skipped.");
return;
}
// Show confirmation dialog
skipConfirmationData.value = { job, date };
showSkipConfirmation.value = true;
};
const performResize = async (data, skipDays) => {
const { job, newEndDate, originalEndDate } = data;
const confirmSkipDay = async () => {
const { job, date } = skipConfirmationData.value;
// Update the job with new end date and skipDays
// Add to skipDays
const newSkipDays = [...(job.skipDays || []), { date }];
// Update local
const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex !== -1) {
const originalSkipDays = scheduledServices.value[serviceIndex].skipDays;
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
expectedEndDate: newEndDate,
skipDays: skipDays
skipDays: newSkipDays
};
// Call API to persist changes
try {
await Api.updateServiceAppointmentScheduledDates(
job.name,
job.expectedStartDate,
newEndDate,
job.foreman,
skipDays
);
notifications.addSuccess("Job end date updated successfully!");
} catch (error) {
console.error("Error updating job end date:", error);
notifications.addError("Failed to update job end date");
// Revert on error
}
// Call API
try {
await Api.updateServiceAppointmentScheduledDates(
job.name,
job.expectedStartDate,
job.expectedEndDate,
job.foreman,
newSkipDays
);
notifications.addSuccess("Day skipped successfully!");
// Untoggle skip mode
skipMode.value = false;
} catch (error) {
console.error("Error skipping day:", error);
notifications.addError("Failed to skip day");
// Revert
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
expectedEndDate: originalEndDate,
skipDays: originalSkipDays
skipDays: job.skipDays
};
}
}
// Close dialog
showSkipConfirmation.value = false;
skipConfirmationData.value = null;
};
const cancelSkipDay = () => {
showSkipConfirmation.value = false;
skipConfirmationData.value = null;
};
// Handle removing a skipped day
const handleRemoveSkip = async (job, date) => {
// Show confirmation dialog
removeSkipConfirmationData.value = { job, date };
showRemoveSkipConfirmation.value = true;
};
const confirmRemoveSkip = async () => {
const { job, date } = removeSkipConfirmationData.value;
// Remove from skipDays
const newSkipDays = (job.skipDays || []).filter(skip => skip.date !== date);
// Update local
const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: newSkipDays
};
}
// Call API
try {
await Api.updateServiceAppointmentScheduledDates(
job.name,
job.expectedStartDate,
job.expectedEndDate,
job.foreman,
newSkipDays
);
notifications.addSuccess("Skip removed successfully!");
} catch (error) {
console.error("Error removing skip:", error);
notifications.addError("Failed to remove skip");
// Revert
if (serviceIndex !== -1) {
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
skipDays: job.skipDays
};
}
}
// Close dialog
showRemoveSkipConfirmation.value = false;
removeSkipConfirmationData.value = null;
};
const cancelRemoveSkip = () => {
showRemoveSkipConfirmation.value = false;
removeSkipConfirmationData.value = null;
};
// Handle dropping scheduled items back to unscheduled
const handleUnscheduledDragOver = (event) => {
// Only allow dropping if the dragged job is scheduled
if (draggedService.value && draggedService.value.status === 'Scheduled') {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}
};
const handleUnscheduledDragLeave = (event) => {
@ -1284,8 +1298,7 @@ const handleUnscheduledDrop = async (event) => {
draggedService.value.name,
null,
null,
null,
[]
null
)
notifications.addSuccess("Job unscheduled successfully!");
} catch (error) {
@ -1327,8 +1340,7 @@ const handleScheduledDrop = async (job, event, foremanId, date) => {
draggedService.value.name,
date,
draggedService.value.expectedEndDate,
foremanId,
draggedService.value.skipDays || []
foremanId
);
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
@ -1414,24 +1426,37 @@ const handleResize = (event) => {
const proposedDate = parseLocalDate(proposedEndDate);
const saturdayDate = parseLocalDate(weekEndDate);
// Check for holidays and Sundays in the EXTENSION range only (from current end to proposed end)
// Check for holidays in the EXTENSION range only (from current end to proposed end)
const holidaysInRange = getHolidaysInRange(currentEndDate, proposedEndDate);
const sundaysInRange = getSundaysInRange(currentEndDate, proposedEndDate);
const conflictingDates = [...holidaysInRange, ...sundaysInRange];
if (conflictingDates.length > 0) {
// Show prompt
holidayDates.value = conflictingDates;
pendingAction.value = {
type: 'resize',
data: { job: resizingJob.value, newEndDate: proposedEndDate, originalEndDate: currentEndDate }
};
showHolidayPrompt.value = true;
return; // Don't update yet
if (holidaysInRange.length > 0) {
hasHolidayBlock = true;
// Allow the extension - visual split will be handled by getJobSegments
newEndDate = proposedEndDate;
}
// No conflicts, proceed
newEndDate = proposedEndDate;
// Check if the proposed end date extends past Saturday or lands on Sunday
if (proposedDate > saturdayDate || isSunday(proposedEndDate)) {
extendsOverSunday = true;
// Set logical end date to next Monday, but visually cap at Saturday
newEndDate = getNextMonday(weekEndDate);
} else if (!hasHolidayBlock && daysToAdd > 0) {
// Check if any date in the EXTENSION range is Sunday (only if no holiday block)
// Only check from current end to proposed end
const startCheck = parseLocalDate(currentEndDate);
const endCheck = parseLocalDate(proposedEndDate);
for (let d = new Date(startCheck.getTime() + 86400000); d <= endCheck; d.setDate(d.getDate() + 1)) {
const checkDate = toLocalDateString(d);
if (isSunday(checkDate)) {
extendsOverSunday = true;
// Extend to next Monday
newEndDate = getNextMonday(checkDate);
break;
}
}
}
// Show popup if extending over Sunday
showExtendToNextWeekPopup.value = extendsOverSunday;
const serviceIndex = scheduledServices.value.findIndex(s => s.name === resizingJob.value.name);
if (serviceIndex !== -1) {
@ -1475,8 +1500,7 @@ const stopResize = async () => {
job.name,
job.expectedStartDate,
newEndDate,
job.foreman,
job.skipDays || []
job.foreman
);
notifications.addSuccess("Job end date updated successfully!");
} catch (error) {
@ -2064,7 +2088,119 @@ onMounted(async () => {
color: #4caf50;
}
.skipped-day {
.extend-popup {
position: fixed;
top: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(33, 150, 243, 0.95);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
pointer-events: none;
z-index: 10000;
white-space: nowrap;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.3);
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.service-address {
font-size: 0.85em;
color: #555;
display: flex;
align-items: center;
line-height: 1.4;
}
.scheduled-item[draggable="true"]:active {
cursor: grabbing;
}
.priority-urgent {
border-left-color: #f44336 !important;
}
.priority-high {
border-left-color: #ff9800 !important;
}
.priority-medium {
border-left-color: #ffeb3b !important;
}
.priority-low {
border-left-color: #4caf50 !important;
}
.service-title-compact {
font-weight: 600;
font-size: 0.9em;
color: #1976d2;
line-height: 1.2;
}
.service-customer {
font-size: 0.8em;
color: #666;
margin-top: 2px;
}
.service-compact-details {
display: flex;
align-items: center;
gap: 8px;
}
.service-notes-compact {
background-color: #f8f9fa;
padding: 4px 6px;
border-radius: 3px;
border-left: 2px solid #2196f3;
}
.service-notes-compact .text-caption {
font-style: italic;
color: #666;
line-height: 1.3;
}
.no-unscheduled {
text-align: center;
padding: 40px 20px;
color: #666;
}
.day-cell.holiday {
background: repeating-linear-gradient(
45deg,
rgba(255, 193, 7, 0.15),
rgba(255, 193, 7, 0.15) 10px,
rgba(255, 193, 7, 0.05) 10px,
rgba(255, 193, 7, 0.05) 20px
);
border-left: 3px solid #ffc107;
border-right: 3px solid #ffc107;
}
.day-cell.sunday {
background-color: rgba(200, 200, 200, 0.1); /* light gray for Sunday */
}
.holiday-connector {
position: absolute;
left: 4px;
right: 4px;
@ -2077,43 +2213,19 @@ onMounted(async () => {
z-index: 5;
}
.skipped-day .remove-skip-btn {
display: none;
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
border: 1px solid #ccc;
cursor: pointer;
z-index: 10;
pointer-events: auto;
padding: 0;
}
.skipped-day:hover .remove-skip-btn {
display: block;
}
.skipped-day .remove-skip-btn:hover {
background: #f5f5f5;
}
.skipped-day.priority-urgent {
.holiday-connector.priority-urgent {
color: #f44336;
}
.skipped-day.priority-high {
.holiday-connector.priority-high {
color: #ff9800;
}
.skipped-day.priority-medium {
.holiday-connector.priority-medium {
color: #2196f3;
}
.skipped-day.priority-low {
.holiday-connector.priority-low {
color: #4caf50;
}
@ -2162,4 +2274,41 @@ onMounted(async () => {
.calendar-container.skip-mode .day-cell {
cursor: crosshair;
}
.skipped-day {
position: relative;
border-top: 2px dotted #ff0000;
border-bottom: 2px dotted #ff0000;
background: rgba(255, 0, 0, 0.05);
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #ff0000;
font-weight: 500;
}
.remove-skip-btn {
position: absolute;
top: 2px;
right: 2px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
color: #ff0000;
border: 1px solid #ff0000;
opacity: 0;
transition: opacity 0.2s;
}
.skipped-day:hover .remove-skip-btn {
opacity: 1;
}
</style>