update
This commit is contained in:
parent
8c818f8dde
commit
e2746b83bb
2 changed files with 412 additions and 383 deletions
|
|
@ -6,10 +6,12 @@ import frappe
|
||||||
from .utils import create_module
|
from .utils import create_module
|
||||||
import holidays
|
import holidays
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
|
||||||
|
|
||||||
|
|
||||||
def after_install():
|
def after_install():
|
||||||
create_module()
|
create_module()
|
||||||
add_custom_fields()
|
# add_custom_fields()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
# Proper way to refresh metadata
|
# Proper way to refresh metadata
|
||||||
|
|
@ -31,7 +33,7 @@ def after_install():
|
||||||
|
|
||||||
def after_migrate():
|
def after_migrate():
|
||||||
add_custom_fields()
|
add_custom_fields()
|
||||||
update_onsite_meeting_fields()
|
# update_onsite_meeting_fields()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
# Proper way to refresh metadata for all doctypes with custom fields
|
# Proper way to refresh metadata for all doctypes with custom fields
|
||||||
|
|
@ -40,13 +42,13 @@ def after_migrate():
|
||||||
frappe.clear_cache(doctype=doctype)
|
frappe.clear_cache(doctype=doctype)
|
||||||
frappe.reload_doctype(doctype)
|
frappe.reload_doctype(doctype)
|
||||||
|
|
||||||
check_and_create_holiday_list()
|
# check_and_create_holiday_list()
|
||||||
# create_project_templates()
|
# create_project_templates()
|
||||||
# create_task_types()
|
# create_task_types()
|
||||||
# create_tasks()
|
# create_tasks()
|
||||||
create_bid_meeting_note_form_templates()
|
# create_bid_meeting_note_form_templates()
|
||||||
create_accounts()
|
create_accounts()
|
||||||
create_companies()
|
# create_companies()
|
||||||
# init_stripe_accounts()
|
# init_stripe_accounts()
|
||||||
|
|
||||||
# update_address_fields()
|
# update_address_fields()
|
||||||
|
|
@ -1592,126 +1594,24 @@ def create_bid_meeting_note_form_templates():
|
||||||
|
|
||||||
doc.insert(ignore_permissions=True)
|
doc.insert(ignore_permissions=True)
|
||||||
|
|
||||||
def create_companies():
|
def create_accounts():
|
||||||
"""Create necessary companies if they do not exist."""
|
"""Create necessary companies if they do not exist."""
|
||||||
print("\n🔧 Checking for necessary companies...")
|
print("\n🔧 Checking for necessary companies...")
|
||||||
|
|
||||||
companies = [
|
companies = frappe.get_all("Company", pluck="name")
|
||||||
{
|
|
||||||
'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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
for company in companies:
|
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
|
continue
|
||||||
data = {
|
company_doc = frappe.get_doc("Company", company)
|
||||||
"doctype": "Company"
|
create_charts(
|
||||||
}
|
company=company.name,
|
||||||
data.update(company)
|
chart_template=company_doc.chart_template
|
||||||
doc = frappe.get_doc(data)
|
)
|
||||||
doc.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
|
|
||||||
def create_accounts():
|
def create_stripe_accounts():
|
||||||
"""Create necessary accounts if they do not exist."""
|
"""Create necessary accounts if they do not exist."""
|
||||||
print("\n🔧 Checking for necessary accounts...")
|
print("\n🔧 Checking for necessary accounts...")
|
||||||
|
|
||||||
|
|
@ -1745,23 +1645,3 @@ def create_accounts():
|
||||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||||
|
|
||||||
frappe.db.commit()
|
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-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- Holiday Prompt Dialog -->
|
<!-- Skip Day Confirmation Dialog -->
|
||||||
<v-dialog v-model="showHolidayPrompt" max-width="500px">
|
<v-dialog v-model="showSkipConfirmation" max-width="400px">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>Holiday Scheduling</v-card-title>
|
<v-card-title>Skip Day Confirmation</v-card-title>
|
||||||
<v-card-text>
|
<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-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn @click="handleHolidayChoice(false)">Include Holiday</v-btn>
|
<v-btn @click="cancelSkipDay">Cancel</v-btn>
|
||||||
<v-btn color="primary" @click="handleHolidayChoice(true)">Skip Holiday</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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
@ -220,6 +235,7 @@
|
||||||
'holiday': isHoliday(day.date),
|
'holiday': isHoliday(day.date),
|
||||||
'sunday': isSunday(day.date),
|
'sunday': isSunday(day.date),
|
||||||
'drag-over': isDragOver && dragOverCell?.foremanId === foreman.name && dragOverCell?.date === 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)"
|
@dragover="handleDragOver($event, foreman.name, day.date)"
|
||||||
@dragleave="handleDragLeave"
|
@dragleave="handleDragLeave"
|
||||||
|
|
@ -233,7 +249,7 @@
|
||||||
class="calendar-job"
|
class="calendar-job"
|
||||||
:style="getJobStyle(job, day.date)"
|
:style="getJobStyle(job, day.date)"
|
||||||
:draggable="job.status === 'Scheduled'"
|
: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"
|
@dragstart="job.status === 'Scheduled' ? handleDragStart(job, $event) : null"
|
||||||
@dragend="handleDragEnd"
|
@dragend="handleDragEnd"
|
||||||
@mousedown="(job.status === 'Scheduled' || job.status === 'Started') ? startResize($event, job, day.date) : null"
|
@mousedown="(job.status === 'Scheduled' || job.status === 'Started') ? startResize($event, job, day.date) : null"
|
||||||
|
|
@ -256,6 +272,7 @@
|
||||||
class="skipped-day"
|
class="skipped-day"
|
||||||
:class="getPriorityClass(job.priority)"
|
:class="getPriorityClass(job.priority)"
|
||||||
>
|
>
|
||||||
|
<span>Skipped</span>
|
||||||
<button
|
<button
|
||||||
class="remove-skip-btn"
|
class="remove-skip-btn"
|
||||||
@click.stop="handleRemoveSkip(job, day.date)"
|
@click.stop="handleRemoveSkip(job, day.date)"
|
||||||
|
|
@ -264,6 +281,8 @@
|
||||||
<v-icon size="small">mdi-close</v-icon>
|
<v-icon size="small">mdi-close</v-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Holiday connector line for split jobs -->
|
||||||
<template v-if="isHoliday(day.date)">
|
<template v-if="isHoliday(day.date)">
|
||||||
<div
|
<div
|
||||||
v-for="job in getJobsWithConnector(foreman.name, day.date)"
|
v-for="job in getJobsWithConnector(foreman.name, day.date)"
|
||||||
|
|
@ -376,9 +395,23 @@ import JobDetailsModal from "../../modals/JobDetailsModal.vue";
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
const companyStore = useCompanyStore();
|
const companyStore = useCompanyStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const random = 3
|
||||||
const serviceAptToFind = route.query.apt || null;
|
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
|
// Reactive data
|
||||||
const scheduledServices = ref([]);
|
const scheduledServices = ref([]);
|
||||||
const unscheduledServices = ref([]);
|
const unscheduledServices = ref([]);
|
||||||
|
|
@ -403,6 +436,9 @@ const resizeStartDate = ref(null);
|
||||||
const originalEndDate = ref(null);
|
const originalEndDate = ref(null);
|
||||||
const justFinishedResize = ref(false);
|
const justFinishedResize = ref(false);
|
||||||
|
|
||||||
|
// Skip mode
|
||||||
|
const skipMode = ref(false);
|
||||||
|
|
||||||
// Foremen data (Crews)
|
// Foremen data (Crews)
|
||||||
const foremen = ref([]);
|
const foremen = ref([]);
|
||||||
|
|
||||||
|
|
@ -414,13 +450,13 @@ const showForemenMenu = ref(false);
|
||||||
const showDatePicker = ref(false);
|
const showDatePicker = ref(false);
|
||||||
const selectedDate = ref(null);
|
const selectedDate = ref(null);
|
||||||
|
|
||||||
// Holiday prompt
|
// Skip day confirmation
|
||||||
const showHolidayPrompt = ref(false);
|
const showSkipConfirmation = ref(false);
|
||||||
const pendingAction = ref(null); // { type: 'drop' or 'resize', data: {...} }
|
const skipConfirmationData = ref(null);
|
||||||
const holidayDates = ref([]); // dates that are holidays in the range
|
|
||||||
|
|
||||||
// Skip mode
|
// Remove skip confirmation
|
||||||
const skipMode = ref(false);
|
const showRemoveSkipConfirmation = ref(false);
|
||||||
|
const removeSkipConfirmationData = ref(null);
|
||||||
|
|
||||||
// Project template filter
|
// Project template filter
|
||||||
const selectedProjectTemplates = ref([]);
|
const selectedProjectTemplates = ref([]);
|
||||||
|
|
@ -561,21 +597,7 @@ function getCrewName(foremanId) {
|
||||||
return foreman.customCrew ? `${foreman.employeeName} (Crew ${foreman.customCrew})` : foreman.employeeName;
|
return foreman.customCrew ? `${foreman.employeeName} (Crew ${foreman.customCrew})` : foreman.employeeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get all Sundays in a date range
|
// Helper function to calculate job segments (parts between holidays/Sundays)
|
||||||
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)
|
|
||||||
function getJobSegments(job) {
|
function getJobSegments(job) {
|
||||||
const startDate = job.expectedStartDate;
|
const startDate = job.expectedStartDate;
|
||||||
const endDate = job.expectedEndDate || job.expectedStartDate;
|
const endDate = job.expectedEndDate || job.expectedStartDate;
|
||||||
|
|
@ -827,108 +849,6 @@ const toggleSkipMode = () => {
|
||||||
skipMode.value = !skipMode.value;
|
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
|
// Foremen selection methods
|
||||||
const toggleAllForemen = () => {
|
const toggleAllForemen = () => {
|
||||||
if (selectedForemen.value.length === foremen.value.length) {
|
if (selectedForemen.value.length === foremen.value.length) {
|
||||||
|
|
@ -1095,6 +1015,25 @@ const handleDrop = async (event, foremanId, date) => {
|
||||||
|
|
||||||
if (!draggedService.value) return;
|
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
|
// Get foreman details
|
||||||
const foreman = foremen.value.find(f => f.name === foremanId);
|
const foreman = foremen.value.find(f => f.name === foremanId);
|
||||||
if (!foreman) return;
|
if (!foreman) return;
|
||||||
|
|
@ -1102,29 +1041,16 @@ const handleDrop = async (event, foremanId, date) => {
|
||||||
// Default to single day
|
// Default to single day
|
||||||
let endDate = date;
|
let endDate = date;
|
||||||
|
|
||||||
// Check for holidays or Sundays in the range
|
// Check for holidays in the range
|
||||||
const holidaysInRange = getHolidaysInRange(date, endDate);
|
if (hasHolidayInRange(date, endDate)) {
|
||||||
const sundaysInRange = getSundaysInRange(date, endDate);
|
notifications.addError("Cannot schedule job on a holiday. Please select different dates.");
|
||||||
const conflictingDates = [...holidaysInRange, ...sundaysInRange];
|
// Reset drag state
|
||||||
|
|
||||||
if (conflictingDates.length > 0) {
|
|
||||||
// Show prompt
|
|
||||||
holidayDates.value = conflictingDates;
|
|
||||||
pendingAction.value = {
|
|
||||||
type: 'drop',
|
|
||||||
data: { event, foremanId, date, endDate, foreman }
|
|
||||||
};
|
|
||||||
showHolidayPrompt.value = true;
|
|
||||||
isDragOver.value = false;
|
isDragOver.value = false;
|
||||||
dragOverCell.value = null;
|
dragOverCell.value = null;
|
||||||
|
draggedService.value = null;
|
||||||
return;
|
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
|
// 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 unscheduledIndex = unscheduledServices.value.findIndex(s => s.name === draggedService.value.name);
|
||||||
const scheduledIndex = scheduledServices.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,
|
draggedService.value.name,
|
||||||
date,
|
date,
|
||||||
endDate,
|
endDate,
|
||||||
foreman.name,
|
foreman.name
|
||||||
skipDays
|
|
||||||
);
|
);
|
||||||
// Remove from unscheduled and add to scheduled
|
// Remove from unscheduled and add to scheduled
|
||||||
const scheduledService = {
|
const scheduledService = {
|
||||||
|
|
@ -1149,8 +1074,7 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
|
||||||
expectedStartDate: date,
|
expectedStartDate: date,
|
||||||
expectedEndDate: endDate,
|
expectedEndDate: endDate,
|
||||||
foreman: foreman.name,
|
foreman: foreman.name,
|
||||||
status: 'Scheduled',
|
status: 'Scheduled'
|
||||||
skipDays: skipDays
|
|
||||||
};
|
};
|
||||||
unscheduledServices.value.splice(unscheduledIndex, 1);
|
unscheduledServices.value.splice(unscheduledIndex, 1);
|
||||||
scheduledServices.value.push(scheduledService);
|
scheduledServices.value.push(scheduledService);
|
||||||
|
|
@ -1169,16 +1093,14 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
|
||||||
draggedService.value.name,
|
draggedService.value.name,
|
||||||
date,
|
date,
|
||||||
date, // Reset to single day when moved
|
date, // Reset to single day when moved
|
||||||
foreman.name,
|
foreman.name
|
||||||
skipDays
|
|
||||||
);
|
);
|
||||||
// Update the scheduled job
|
// Update the scheduled job
|
||||||
scheduledServices.value[scheduledIndex] = {
|
scheduledServices.value[scheduledIndex] = {
|
||||||
...scheduledServices.value[scheduledIndex],
|
...scheduledServices.value[scheduledIndex],
|
||||||
expectedStartDate: date,
|
expectedStartDate: date,
|
||||||
expectedEndDate: date, // Reset to single day
|
expectedEndDate: date, // Reset to single day
|
||||||
foreman: foreman.name,
|
foreman: foreman.name
|
||||||
skipDays: skipDays
|
|
||||||
};
|
};
|
||||||
notifications.addSuccess("Job moved successfully!");
|
notifications.addSuccess("Job moved successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1193,58 +1115,150 @@ const performDrop = async (foremanId, date, endDate, foreman, skipDays = []) =>
|
||||||
draggedService.value = null;
|
draggedService.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHolidayChoice = async (skip) => {
|
const handleSkipDayClick = async (foremanId, date, specificJob = null) => {
|
||||||
showHolidayPrompt.value = false;
|
let job;
|
||||||
const action = pendingAction.value;
|
if (specificJob) {
|
||||||
if (!action) return;
|
job = specificJob;
|
||||||
|
} else {
|
||||||
const skipDays = skip ? holidayDates.value : [];
|
// Find jobs that cover this date for this foreman
|
||||||
|
const jobs = scheduledServices.value.filter(job => {
|
||||||
if (action.type === 'drop') {
|
if (job.foreman !== foremanId) return false;
|
||||||
const { foremanId, date, endDate, foreman } = action.data;
|
const start = job.expectedStartDate;
|
||||||
await performDrop(foremanId, date, endDate, foreman, skipDays);
|
const end = job.expectedEndDate || start;
|
||||||
} else if (action.type === 'resize') {
|
return date >= start && date <= end;
|
||||||
await performResize(action.data, skipDays);
|
});
|
||||||
|
|
||||||
|
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;
|
// Check if already skipped
|
||||||
holidayDates.value = [];
|
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 confirmSkipDay = async () => {
|
||||||
const { job, newEndDate, originalEndDate } = data;
|
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);
|
const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
|
||||||
if (serviceIndex !== -1) {
|
if (serviceIndex !== -1) {
|
||||||
const originalSkipDays = scheduledServices.value[serviceIndex].skipDays;
|
|
||||||
scheduledServices.value[serviceIndex] = {
|
scheduledServices.value[serviceIndex] = {
|
||||||
...scheduledServices.value[serviceIndex],
|
...scheduledServices.value[serviceIndex],
|
||||||
expectedEndDate: newEndDate,
|
skipDays: newSkipDays
|
||||||
skipDays: skipDays
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
// Call API to persist changes
|
|
||||||
try {
|
// Call API
|
||||||
await Api.updateServiceAppointmentScheduledDates(
|
try {
|
||||||
job.name,
|
await Api.updateServiceAppointmentScheduledDates(
|
||||||
job.expectedStartDate,
|
job.name,
|
||||||
newEndDate,
|
job.expectedStartDate,
|
||||||
job.foreman,
|
job.expectedEndDate,
|
||||||
skipDays
|
job.foreman,
|
||||||
);
|
newSkipDays
|
||||||
notifications.addSuccess("Job end date updated successfully!");
|
);
|
||||||
} catch (error) {
|
notifications.addSuccess("Day skipped successfully!");
|
||||||
console.error("Error updating job end date:", error);
|
// Untoggle skip mode
|
||||||
notifications.addError("Failed to update job end date");
|
skipMode.value = false;
|
||||||
// Revert on error
|
} catch (error) {
|
||||||
|
console.error("Error skipping day:", error);
|
||||||
|
notifications.addError("Failed to skip day");
|
||||||
|
// Revert
|
||||||
|
if (serviceIndex !== -1) {
|
||||||
scheduledServices.value[serviceIndex] = {
|
scheduledServices.value[serviceIndex] = {
|
||||||
...scheduledServices.value[serviceIndex],
|
...scheduledServices.value[serviceIndex],
|
||||||
expectedEndDate: originalEndDate,
|
skipDays: job.skipDays
|
||||||
skipDays: originalSkipDays
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
const handleUnscheduledDragLeave = (event) => {
|
||||||
|
|
@ -1284,8 +1298,7 @@ const handleUnscheduledDrop = async (event) => {
|
||||||
draggedService.value.name,
|
draggedService.value.name,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
notifications.addSuccess("Job unscheduled successfully!");
|
notifications.addSuccess("Job unscheduled successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1327,8 +1340,7 @@ const handleScheduledDrop = async (job, event, foremanId, date) => {
|
||||||
draggedService.value.name,
|
draggedService.value.name,
|
||||||
date,
|
date,
|
||||||
draggedService.value.expectedEndDate,
|
draggedService.value.expectedEndDate,
|
||||||
foremanId,
|
foremanId
|
||||||
draggedService.value.skipDays || []
|
|
||||||
);
|
);
|
||||||
scheduledServices.value[serviceIndex] = {
|
scheduledServices.value[serviceIndex] = {
|
||||||
...scheduledServices.value[serviceIndex],
|
...scheduledServices.value[serviceIndex],
|
||||||
|
|
@ -1414,24 +1426,37 @@ const handleResize = (event) => {
|
||||||
const proposedDate = parseLocalDate(proposedEndDate);
|
const proposedDate = parseLocalDate(proposedEndDate);
|
||||||
const saturdayDate = parseLocalDate(weekEndDate);
|
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 holidaysInRange = getHolidaysInRange(currentEndDate, proposedEndDate);
|
||||||
const sundaysInRange = getSundaysInRange(currentEndDate, proposedEndDate);
|
if (holidaysInRange.length > 0) {
|
||||||
const conflictingDates = [...holidaysInRange, ...sundaysInRange];
|
hasHolidayBlock = true;
|
||||||
|
// Allow the extension - visual split will be handled by getJobSegments
|
||||||
if (conflictingDates.length > 0) {
|
newEndDate = proposedEndDate;
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No conflicts, proceed
|
// Check if the proposed end date extends past Saturday or lands on Sunday
|
||||||
newEndDate = proposedEndDate;
|
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);
|
const serviceIndex = scheduledServices.value.findIndex(s => s.name === resizingJob.value.name);
|
||||||
if (serviceIndex !== -1) {
|
if (serviceIndex !== -1) {
|
||||||
|
|
@ -1475,8 +1500,7 @@ const stopResize = async () => {
|
||||||
job.name,
|
job.name,
|
||||||
job.expectedStartDate,
|
job.expectedStartDate,
|
||||||
newEndDate,
|
newEndDate,
|
||||||
job.foreman,
|
job.foreman
|
||||||
job.skipDays || []
|
|
||||||
);
|
);
|
||||||
notifications.addSuccess("Job end date updated successfully!");
|
notifications.addSuccess("Job end date updated successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2064,7 +2088,119 @@ onMounted(async () => {
|
||||||
color: #4caf50;
|
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;
|
position: absolute;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
|
@ -2077,43 +2213,19 @@ onMounted(async () => {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skipped-day .remove-skip-btn {
|
.holiday-connector.priority-urgent {
|
||||||
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 {
|
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skipped-day.priority-high {
|
.holiday-connector.priority-high {
|
||||||
color: #ff9800;
|
color: #ff9800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skipped-day.priority-medium {
|
.holiday-connector.priority-medium {
|
||||||
color: #2196f3;
|
color: #2196f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skipped-day.priority-low {
|
.holiday-connector.priority-low {
|
||||||
color: #4caf50;
|
color: #4caf50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2162,4 +2274,41 @@ onMounted(async () => {
|
||||||
.calendar-container.skip-mode .day-cell {
|
.calendar-container.skip-mode .day-cell {
|
||||||
cursor: crosshair;
|
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>
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue