update
This commit is contained in:
parent
8c818f8dde
commit
e2746b83bb
@ -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()
|
||||
|
||||
@ -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>
|
||||
Loading…
x
Reference in New Issue
Block a user