<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lead Management</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;500;600;700&family=IBM+Plex+Mono:wght@500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2c3e50;
--accent-color: #e74c3c;
--light-bg: #f5f5f5;
--card-bg: #ffffff;
--border-color: #ddd;
--text-color: #333;
--success-color: #2ecc71;
--warning-color: #f39c12;
--shadow-sm: 0 2px 5px rgba(0,0,0,0.1);
--shadow-md: 0 4px 10px rgba(0,0,0,0.15);
--radius-sm: 4px;
--radius-md: 8px;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
max-width: 1400px;
margin: 0 auto;
padding: 15px;
background: linear-gradient(135deg, var(--light-bg) 0%, #e8f4f8 100%);
font-size: 14px;
}
h1 {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
text-align: center;
margin-bottom: 20px;
}
h2, h3, h4 {
color: var(--secondary-color);
margin-top: 0;
}
.container {
background-color: var(--card-bg);
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-sm);
margin-bottom: 20px;
transition: transform 0.2s;
}
.container:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
font-size: 13px;
color: var(--secondary-color);
}
input, select, textarea {
width: 100%;
padding: 10px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
box-sizing: border-box;
font-size: 14px;
transition: border-color 0.3s, box-shadow 0.3s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
button {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
border: none;
padding: 12px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 14px;
font-weight: 600;
margin-right: 8px;
margin-top: 8px;
transition: all 0.3s;
box-shadow: var(--shadow-sm);
}
button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
button:active {
transform: translateY(0);
}
button i {
margin-right: 5px;
}
.share-buttons, .offer-buttons {
margin-top: 15px;
border-top: 2px solid var(--border-color);
padding-top: 15px;
}
.lead-display, .profit-display, .offer-display, .projection-display {
background: linear-gradient(135deg, #f9f9f9, #ffffff);
padding: 15px;
border-radius: var(--radius-sm);
border-left: 4px solid var(--primary-color);
margin-top: 15px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 13px;
box-shadow: var(--shadow-sm);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
font-size: 13px;
background: white;
box-shadow: var(--shadow-sm);
border-radius: var(--radius-sm);
overflow: hidden;
}
th, td {
border: 1px solid var(--border-color);
padding: 12px;
text-align: left;
}
th {
background: linear-gradient(135deg, var(--secondary-color), #34495e);
color: white;
cursor: pointer;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
th:hover {
background: linear-gradient(135deg, #34495e, var(--secondary-color));
}
tr:hover {
background-color: #f0f8ff;
}
.tabs {
display: flex;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 8px;
background: white;
padding: 10px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.tabs.horizontal {
flex-direction: row;
}
.tabs.vertical {
flex-direction: column;
width: 200px;
float: left;
margin-right: 20px;
}
.tabs.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.tabs.compact {
flex-direction: row;
flex-wrap: wrap;
}
.tabs.compact .tab {
flex: 1;
min-width: 120px;
}
.tab {
padding: 12px 20px;
background: linear-gradient(135deg, #eee, #f5f5f5);
cursor: pointer;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
border: 2px solid transparent;
}
.tab:hover {
background: linear-gradient(135deg, #e0e0e0, #eee);
transform: translateY(-2px);
}
.tab.active {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
}
.tab i {
margin-right: 8px;
}
.tab-content {
display: none;
animation: fadeIn 0.3s;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: linear-gradient(135deg, white, #f8f9fa);
padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: all 0.3s;
border-left: 4px solid var(--primary-color);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: var(--primary-color);
margin: 10px 0;
}
.stat-icon {
font-size: 32px;
margin-bottom: 10px;
color: var(--primary-color);
}
.chart-container {
position: relative;
height: 300px;
margin-bottom: 20px;
background: white;
padding: 15px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.customer-list {
max-height: 500px;
overflow-y: auto;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 10px;
}
.customer-card, .tech-card-item, .partner-card-item {
background: linear-gradient(135deg, white, #f8f9fa);
padding: 15px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
border-left: 4px solid var(--primary-color);
}
.customer-card:hover, .tech-card-item:hover, .partner-card-item:hover {
background: linear-gradient(135deg, #e3f2fd, #ffffff);
transform: translateX(5px);
box-shadow: var(--shadow-md);
}
.offer-group {
margin-bottom: 15px;
padding: 15px;
background: linear-gradient(135deg, #f0f0f0, #f8f8f8);
border-radius: var(--radius-md);
border-left: 4px solid var(--primary-color);
}
.offer-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
background: white;
border-radius: var(--radius-sm);
transition: background 0.3s;
}
.offer-item:hover {
background: #e3f2fd;
}
.offer-item input[type="checkbox"] {
margin-right: 10px;
width: 20px;
height: 20px;
cursor: pointer;
}
.data-import-export {
margin-top: 15px;
padding: 20px;
background: linear-gradient(135deg, #f9f9f9, white);
border-radius: var(--radius-md);
border-left: 4px solid var(--warning-color);
}
.projection-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 15px;
}
.projection-card {
background: linear-gradient(135deg, white, #f8f9fa);
padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
margin-bottom: 15px;
border-left: 4px solid var(--success-color);
}
.lead-filter {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.lead-filter button {
margin-top: 0;
padding: 10px 20px;
}
.lead-filter button.active {
background: linear-gradient(135deg, var(--secondary-color), #34495e);
}
.partner-section {
margin-top: 15px;
padding: 20px;
background: linear-gradient(135deg, #e3f2fd, white);
border-radius: var(--radius-md);
border-left: 4px solid var(--primary-color);
}
.profit-share-buttons, .offer-share-buttons {
margin-top: 15px;
}
.commission-slider {
width: 100%;
margin: 15px 0;
height: 8px;
border-radius: 5px;
outline: none;
-webkit-appearance: none;
background: linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) 50%, #ddd 50%, #ddd 100%);
}
.commission-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
box-shadow: var(--shadow-sm);
}
.commission-slider::-moz-range-thumb {
width: 25px;
height: 25px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
box-shadow: var(--shadow-sm);
}
.slider-container {
padding: 10px 0;
}
.slider-value {
font-weight: bold;
text-align: center;
margin-top: 8px;
font-size: 18px;
color: var(--primary-color);
}
.customer-sheet {
margin-top: 15px;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.sheet-header {
background: linear-gradient(135deg, var(--secondary-color), #34495e);
color: white;
padding: 15px;
font-weight: bold;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.3s;
}
.sheet-header:hover {
background: linear-gradient(135deg, #34495e, var(--secondary-color));
}
.sheet-header i {
transition: transform 0.3s;
}
.sheet-header.collapsed i {
transform: rotate(-90deg);
}
.sheet-content {
padding: 20px;
background-color: var(--card-bg);
}
.sheet-row {
display: grid;
grid-template-columns: 200px 1fr;
margin-bottom: 12px;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.sheet-label {
font-weight: bold;
color: var(--secondary-color);
}
.sheet-value {
word-break: break-word;
}
.job-history, .offer-history {
margin-top: 15px;
}
.job-item {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 15px;
margin-bottom: 15px;
background: linear-gradient(135deg, white, #f8f9fa);
position: relative;
box-shadow: var(--shadow-sm);
transition: all 0.3s;
}
.job-item:hover {
transform: translateX(5px);
box-shadow: var(--shadow-md);
}
.job-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-weight: bold;
color: var(--secondary-color);
}
.job-edit-btn, .offer-edit-btn {
position: absolute;
top: 15px;
right: 15px;
padding: 8px 15px;
font-size: 12px;
margin: 0;
}
.job-delete-btn {
position: absolute;
top: 15px;
right: 100px;
padding: 8px 15px;
font-size: 12px;
margin: 0;
}
.offer-item-card {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 15px;
margin-bottom: 15px;
background: linear-gradient(135deg, white, #f8f9fa);
position: relative;
box-shadow: var(--shadow-sm);
}
.offer-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-weight: bold;
}
.offer-services {
margin-top: 10px;
}
.offer-delete-btn {
position: absolute;
top: 15px;
right: 100px;
padding: 8px 15px;
font-size: 12px;
margin: 0;
}
.tech-section {
margin-top: 15px;
}
.tech-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.tech-stat {
text-align: center;
padding: 15px;
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
.tech-stat-value {
font-weight: bold;
font-size: 24px;
color: var(--primary-color);
}
.tech-stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.danger-button {
background: linear-gradient(135deg, var(--accent-color), #c0392b);
}
.danger-button:hover {
background: linear-gradient(135deg, #c0392b, var(--accent-color));
}
.success-button {
background: linear-gradient(135deg, var(--success-color), #27ae60);
}
.success-button:hover {
background: linear-gradient(135deg, #27ae60, var(--success-color));
}
.warning-button {
background: linear-gradient(135deg, var(--warning-color), #d35400);
}
.warning-button:hover {
background: linear-gradient(135deg, #d35400, var(--warning-color));
}
.compact-section {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: 15px;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.compact-header {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
padding: 15px;
font-weight: bold;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
}
.compact-header:hover {
background: linear-gradient(135deg, #e8e8e8, #f5f5f5);
}
.compact-header.active {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
}
.compact-header i {
transition: transform 0.3s;
}
.compact-header.collapsed i {
transform: rotate(-90deg);
}
.compact-content {
padding: 20px;
background-color: var(--card-bg);
display: none;
}
.compact-content.expanded {
display: block;
}
.lead-cost-analysis {
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-md);
padding: 20px;
margin-top: 15px;
border-left: 4px solid var(--primary-color);
box-shadow: var(--shadow-sm);
}
.lead-cost-result {
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
padding: 15px;
border-radius: var(--radius-sm);
font-weight: bold;
margin-top: 15px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: var(--shadow-sm);
}
.lead-cost-result i {
font-size: 24px;
color: var(--primary-color);
}
.lead-cost-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 15px;
}
.lead-cost-metric {
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.lead-cost-metric h4 {
margin-top: 0;
display: flex;
align-items: center;
gap: 10px;
color: var(--secondary-color);
}
.lead-cost-metric-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
color: var(--primary-color);
}
.job-totals {
margin-top: 15px;
padding: 20px;
background: linear-gradient(135deg, #e3f2fd, white);
border-radius: var(--radius-md);
font-weight: bold;
box-shadow: var(--shadow-sm);
}
.job-totals-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.job-total-item {
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
padding: 15px;
border-radius: var(--radius-sm);
text-align: center;
box-shadow: var(--shadow-sm);
transition: transform 0.3s;
}
.job-total-item:hover {
transform: translateY(-5px);
}
.job-total-value {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
margin: 10px 0;
}
.job-total-label {
font-size: 12px;
color: #666;
}
.date-range-filter {
display: flex;
gap: 15px;
margin-bottom: 15px;
align-items: flex-end;
flex-wrap: wrap;
}
.date-range-filter button {
margin-top: 0;
}
.strategy-card, .mistake-card {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 15px;
background: linear-gradient(135deg, white, #f8f9fa);
box-shadow: var(--shadow-sm);
position: relative;
}
.strategy-header, .mistake-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-weight: bold;
color: var(--secondary-color);
}
.strategy-actions, .mistake-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid var(--border-color);
}
.payment-section {
margin-top: 15px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-md);
border-left: 4px solid var(--primary-color);
box-shadow: var(--shadow-sm);
}
.payment-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 15px;
}
.payment-history {
margin-top: 15px;
}
.payment-item {
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 15px;
margin-bottom: 10px;
background: white;
box-shadow: var(--shadow-sm);
}
.payment-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-weight: bold;
}
.payment-status {
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.payment-status.paid {
background-color: var(--success-color);
color: white;
}
.payment-status.pending {
background-color: var(--warning-color);
color: white;
}
.payment-status.overdue {
background-color: var(--accent-color);
color: white;
}
.payment-balance {
display: flex;
justify-content: space-between;
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid var(--border-color);
font-weight: bold;
font-size: 18px;
}
.export-buttons {
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid var(--border-color);
}
.completed-jobs-section {
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.completed-jobs-list {
max-height: 600px;
overflow-y: auto;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
animation: fadeIn 0.3s;
}
.modal-content {
background-color: #fefefe;
margin: 3% auto;
padding: 30px;
border: none;
border-radius: var(--radius-md);
width: 90%;
max-width: 700px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.close {
color: #aaa;
float: right;
font-size: 32px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
}
.close:hover,
.close:focus {
color: var(--accent-color);
}
/* Calendar Styles */
.calendar-container {
margin-top: 20px;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.calendar-nav button {
margin: 0 5px;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-bottom: 20px;
}
.calendar-day-header {
text-align: center;
font-weight: bold;
padding: 15px;
background: linear-gradient(135deg, var(--secondary-color), #34495e);
color: white;
border-radius: var(--radius-sm);
}
.calendar-day {
min-height:120px;
padding: 8px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--card-bg), #fafafa);
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.calendar-day:hover {
background: linear-gradient(135deg, #e3f2fd, #ffffff);
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.calendar-day.other-month {
opacity: 0.4;
}
.calendar-day.today {
border: 3px solid var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
box-shadow: 0 0 15px rgba(52, 152, 219, 0.3);
}
.calendar-day-number {
font-weight: bold;
margin-bottom: 8px;
color: var(--secondary-color);
font-size: 16px;
}
.calendar-day-profit {
font-size: 11px;
color: var(--success-color);
font-weight: bold;
margin-top: 5px;
padding: 3px 6px;
background: rgba(46, 204, 113, 0.1);
border-radius: 3px;
}
.calendar-job {
padding: 4px 6px;
margin: 3px 0;
border-radius: var(--radius-sm);
font-size: 11px;
cursor: move;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-shadow: var(--shadow-sm);
transition: all 0.2s;
}
.calendar-job:hover {
transform: scale(1.05);
z-index: 10;
}
.calendar-job.dragging {
opacity: 0.5;
}
.calendar-day.drag-over {
background: linear-gradient(135deg, #fff3cd, #ffffff);
border-color: var(--warning-color);
}
.calendar-job.status-new {
background: linear-gradient(135deg, #64b5f6, #42a5f5);
color: white;
}
.calendar-job.status-inprogress {
background: linear-gradient(135deg, #ffb74d, #ffa726);
color: white;
}
.calendar-job.status-completed {
background: linear-gradient(135deg, #81c784, #66bb6a);
color: white;
}
.calendar-job.status-lost {
background: linear-gradient(135deg, #e57373, #ef5350);
color: white;
}
.calendar-job.status-followup {
background: linear-gradient(135deg, #ba68c8, #ab47bc);
color: white;
}
.calendar-job.reminder-call {
background: linear-gradient(135deg, #26c6da, #00acc1);
color: white;
}
.calendar-job.reminder-email {
background: linear-gradient(135deg, #ffa726, #ff9800);
color: white;
}
.calendar-job.reminder-maintenance {
background: linear-gradient(135deg, #8bc34a, #7cb342);
color: white;
}
.calendar-job.reminder-review {
background: linear-gradient(135deg, #ffca28, #ffc107);
color: white;
}
.calendar-legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 15px;
padding: 15px;
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 25px;
height: 25px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
/* Upcoming Reminders Section */
.upcoming-reminders {
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, white);
border-radius: var(--radius-md);
border-left: 4px solid var(--warning-color);
box-shadow: var(--shadow-sm);
}
.reminder-list {
max-height: 400px;
overflow-y: auto;
}
.reminder-item {
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 15px;
margin-bottom: 10px;
background: white;
box-shadow: var(--shadow-sm);
transition: all 0.3s;
}
.reminder-item:hover {
transform: translateX(5px);
box-shadow: var(--shadow-md);
}
.reminder-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-weight: bold;
}
.reminder-type {
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
color: white;
}
.reminder-type.call {
background-color: #26c6da;
}
.reminder-type.email {
background-color: #ffa726;
}
.reminder-type.maintenance {
background-color: #8bc34a;
}
.reminder-type.review {
background-color: #ffca28;
}
/* Settings Styles */
.settings-section {
margin-bottom: 25px;
padding: 20px;
background: linear-gradient(135deg, var(--card-bg), #f8f9fa);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 15px;
margin-top: 15px;
}
.theme-option {
padding: 25px;
border: 3px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.theme-option:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.theme-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
box-shadow: var(--shadow-md);
}
.theme-colors {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 12px;
}
.theme-color-dot {
width: 25px;
height: 25px;
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
.button-layout-options, .button-size-options {
display: flex;
gap: 15px;
margin-top: 15px;
}
.layout-option, .size-option {
flex: 1;
padding: 20px;
border: 3px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.layout-option:hover, .size-option:hover {
background: linear-gradient(135deg, #f0f8ff, white);
}
.layout-option.active, .size-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.tab-visibility-list {
margin-top: 15px;
}
.tab-visibility-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
margin-bottom: 10px;
background: white;
transition: all 0.3s;
}
.tab-visibility-item:hover {
background: #f8f9fa;
transform: translateX(5px);
}
.tab-visibility-controls {
display: flex;
gap: 15px;
align-items: center;
}
.tab-order-buttons {
display: flex;
gap: 5px;
}
.tab-order-buttons button {
margin: 0;
padding: 8px 12px;
}
.field-visibility-list {
margin-top: 15px;
}
.field-visibility-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
margin-bottom: 8px;
background: white;
}
.offer-select-custom {
margin-bottom: 15px;
}
.offer-dropdown {
position: relative;
}
.offer-dropdown-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, white, #f8f9fa);
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
}
.offer-dropdown-btn:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.offer-dropdown-content {
display: none;
position: absolute;
width: 100%;
max-height: 400px;
overflow-y: auto;
background: white;
border: 2px solid var(--primary-color);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
z-index: 100;
margin-top: 5px;
}
.offer-dropdown-content.show {
display: block;
animation: slideDown 0.3s;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.offer-dropdown-item {
padding: 12px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--border-color);
}
.offer-dropdown-item:hover {
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.offer-dropdown-item input[type="checkbox"] {
margin-right: 10px;
width: 18px;
height: 18px;
}
.partner-experience-section {
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg, #fff3cd, white);
border-radius: var(--radius-md);
border-left: 4px solid var(--warning-color);
}
.experience-item {
padding: 15px;
margin-bottom: 15px;
background: white;
border-radius: var(--radius-sm);
border-left: 4px solid var(--primary-color);
box-shadow: var(--shadow-sm);
position: relative;
}
.experience-actions {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 5px;
}
.experience-actions button {
padding: 6px 12px;
font-size: 12px;
margin: 0;
}
.tech-detail-section {
margin-top: 20px;
}
.tech-data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: var(--radius-sm);
margin-bottom: 8px;
}
.tech-data-actions {
display: flex;
gap: 5px;
}
.tech-data-actions button {
padding: 6px 12px;
font-size: 11px;
margin: 0;
}
/* Button style overrides based on settings */
body.button-layout-icon button span {
display: none;
}
body.button-layout-text button i {
display: none;
}
body.button-size-small button {
padding: 8px 12px;
font-size: 12px;
}
body.button-size-large button {
padding: 14px 24px;
font-size: 16px;
}
/* Dark theme adjustments */
body.theme-dark {
background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
color: #ffffff;
}
body.theme-dark .container,
body.theme-dark .stat-card,
body.theme-dark .customer-card,
body.theme-dark .job-item,
body.theme-dark .offer-item-card,
body.theme-dark .strategy-card,
body.theme-dark .mistake-card {
background: linear-gradient(135deg, #2d2d2d, #3a3a3a);
color: #ffffff;
}
body.theme-dark input,
body.theme-dark select,
body.theme-dark textarea {
background: #2d2d2d;
color: #ffffff;
border-color: #555;
}
body.theme-dark table {
background: #2d2d2d;
}
body.theme-dark td {
color: #ffffff;
}
body.theme-dark .calendar-day {
background: linear-gradient(135deg, #2d2d2d, #3a3a3a);
border-color: #555;
}
body.theme-dark .compact-header,
body.theme-dark .settings-section-header {
background: linear-gradient(135deg, #3a3a3a, #4a4a4a);
color: #ffffff;
}
body.theme-dark .compact-content,
body.theme-dark .settings-section-content {
background: #2d2d2d;
color: #ffffff;
}
/* Green theme adjustments */
body.theme-green {
--primary-color: #4caf50;
--secondary-color: #1b5e20;
--accent-color: #ff5722;
--light-bg: #f1f8e9;
--card-bg: #ffffff;
--border-color: #c8e6c9;
--text-color: #1b5e20;
--success-color: #2e7d32;
--warning-color: #ff8f00;
}
/* Purple theme adjustments */
body.theme-purple {
--primary-color: #9c27b0;
--secondary-color: #4a148c;
--accent-color: #ff6f00;
--light-bg: #f3e5f5;
--card-bg: #ffffff;
--border-color: #e1bee7;
--text-color: #4a148c;
--success-color: #7b1fa2;
--warning-color: #ffab00;
}
/* Ocean theme adjustments */
body.theme-ocean {
--primary-color: #00acc1;
--secondary-color: #006064;
--accent-color: #d84315;
--light-bg: #e0f7fa;
--card-bg: #ffffff;
--border-color: #b2ebf2;
--text-color: #006064;
--success-color: #00838f;
--warning-color: #ff8f00;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
padding: 10px;
}
h1 {
font-size: 20px;
padding: 15px;
}
.two-column,
.projection-controls,
.lead-cost-metrics,
.payment-form {
grid-template-columns: 1fr;
}
.sheet-row {
grid-template-columns: 1fr;
margin-bottom: 15px;
}
.sheet-label {
margin-bottom: 5px;
}
.job-totals-grid {
grid-template-columns: 1fr 1fr;
}
.date-range-filter {
flex-direction: column;
align-items: stretch;
}
.modal-content {
width: 95%;
margin: 10% auto;
padding: 20px;
}
.calendar-grid {
gap: 4px;
}
.calendar-day {
min-height: 90px;
padding: 5px;
}
.calendar-day-number {
font-size: 14px;
}
.calendar-job {
font-size: 10px;
padding: 3px 5px;
}
.button-layout-options,
.button-size-options {
flex-direction: column;
}
.tabs {
gap: 5px;
}
.tab {
padding: 10px 15px;
font-size: 13px;
}
.stats-grid {
grid-template-columns: 1fr;
}
button {
padding: 10px 15px;
font-size: 13px;
}
.job-edit-btn, .job-delete-btn {
position: static;
margin-top: 10px;
}
.container {
padding: 15px;
}
}
@media (max-width: 480px) {
.calendar-grid {
gap: 2px;
}
.calendar-day {
min-height: 70px;
padding: 3px;
}
.calendar-day-number {
font-size: 12px;
}
.calendar-job {
font-size: 9px;
padding: 2px 4px;
}
.stat-value {
font-size: 24px;
}
.job-totals-grid {
grid-template-columns: 1fr;
}
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #2980b9, var(--primary-color));
}
/* NEW STYLES FOR FIXES */
select {
height: 45px;
background: linear-gradient(135deg, #f8f9fa, #ffffff);
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
}
select:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
.time-window-container {
display: flex;
gap: 10px;
align-items: center;
}
.time-window-container select {
flex: 1;
}
.time-window-separator {
font-weight: bold;
color: var(--secondary-color);
}
.time-preset-buttons {
display: flex;
gap: 8px;
margin-top: 8px;
flex-wrap: wrap;
}
.time-preset-button {
padding: 6px 12px;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.time-preset-button:hover {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
border-color: var(--primary-color);
}
.time-preset-button.active {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
border-color: var(--primary-color);
}
/* NEW STYLES FOR LEAD ENTRY SETTINGS */
.service-options-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.service-option {
flex: 1;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.service-option:hover {
background: linear-gradient(135deg, #f0f8ff, white);
}
.service-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.custom-service-input {
display: none;
margin-top: 10px;
}
.custom-service-input.show {
display: flex;
gap: 10px;
}
.service-list-container {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 10px;
background: white;
}
.service-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid var(--border-color);
}
.service-list-item:last-child {
border-bottom: none;
}
.service-list-actions {
display: flex;
gap: 5px;
}
.service-list-actions button {
padding: 4px 8px;
font-size: 11px;
margin: 0;
}
/* Enhanced dropdown styling */
select {
height: 45px;
background: linear-gradient(135deg, #f8f9fa, #ffffff);
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
}
select:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
/* NEW STYLES FOR COMPACT SETTINGS SECTIONS */
.settings-section.compact {
padding: 15px;
}
.settings-section.compact .form-group {
margin-bottom: 10px;
}
.settings-section.compact h3 {
font-size: 16px;
margin-bottom: 10px;
}
.settings-section.compact .field-visibility-item {
padding: 10px;
}
/* NEW STYLES FOR TECHNICIAN DETAILS */
.tech-detail-categories {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin-top: 15px;
}
.tech-detail-category {
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-md);
padding: 15px;
box-shadow: var(--shadow-sm);
}
.tech-detail-category h4 {
margin-top: 0;
color: var(--secondary-color);
border-bottom: 2px solid var(--primary-color);
padding-bottom: 8px;
}
.tech-detail-field {
margin-bottom: 10px;
}
.tech-detail-field label {
font-size: 12px;
margin-bottom: 3px;
}
.tech-detail-field input,
.tech-detail-field select,
.tech-detail-field textarea {
font-size: 13px;
padding: 8px;
}
/* NEW STYLES FOR JOB DETAILS FIELD CONTROL */
.job-field-controls {
margin-top: 15px;
}
.job-field-controls-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.job-field-control-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
}
.job-field-control-item input[type="checkbox"] {
width: 18px;
height: 18px;
}
.job-field-control-item label {
margin: 0;
font-size: 13px;
}
/* NEW STYLES FOR TAB LAYOUT OPTIONS */
.tab-layout-options {
display: flex;
gap: 15px;
margin-top: 15px;
}
.tab-layout-option {
flex: 1;
padding: 20px;
border: 3px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.tab-layout-option:hover {
background: linear-gradient(135deg, #f0f8ff, white);
}
.tab-layout-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.tab-layout-preview {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.tab-layout-preview.horizontal {
flex-direction: row;
gap: 5px;
}
.tab-layout-preview.vertical {
flex-direction: column;
gap: 5px;
align-items: flex-start;
}
.tab-layout-preview.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
}
.tab-layout-preview.compact {
flex-direction: row;
flex-wrap: wrap;
gap: 3px;
}
.tab-layout-preview-item {
padding: 8px 12px;
background: var(--primary-color);
color: white;
border-radius: var(--radius-sm);
font-size: 12px;
}
/* NEW STYLES FOR COMPANY MANAGEMENT */
.company-list-container {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 10px;
background: white;
}
.company-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid var(--border-color);
}
.company-list-item:last-child {
border-bottom: none;
}
.company-list-actions {
display: flex;
gap: 5px;
}
.company-list-actions button {
padding: 4px 8px;
font-size: 11px;
margin: 0;
}
.company-default-badge {
background: var(--success-color);
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
margin-left: 5px;
}
/* NEW STYLES FOR COLLAPSIBLE SETTINGS SECTIONS */
.settings-section-header {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
padding: 15px;
font-weight: bold;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
border-radius: var(--radius-md);
margin-bottom: 0;
}
.settings-section-header:hover {
background: linear-gradient(135deg, #e8e8e8, #f5f5f5);
}
.settings-section-header.active {
background: linear-gradient(135deg, var(--primary-color), #2980b9);
color: white;
}
.settings-section-header i {
transition: transform 0.3s;
}
.settings-section-header.collapsed i {
transform: rotate(-90deg);
}
.settings-section-content {
padding: 20px;
background-color: var(--card-bg);
display: none;
border-radius: 0 0 var(--radius-md) var(--radius-md);
border-top: none;
}
.settings-section-content.expanded {
display: block;
}
/* NEW STYLES FOR TITLE DISPLAY SETTING */
.title-display-options {
display: flex;
gap: 15px;
margin-top: 15px;
}
.title-display-option {
flex: 1;
padding: 20px;
border: 3px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.title-display-option:hover {
background: linear-gradient(135deg, #f0f8ff, white);
}
.title-display-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.title-display-preview {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.title-display-preview h1 {
margin: 0;
font-size: 18px;
padding: 10px 15px;
}
/* NEW STYLES FOR MY ACCOUNT SECTION */
.my-account-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.my-account-stat {
background: linear-gradient(135deg, white, #f8f9fa);
padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
text-align: center;
transition: all 0.3s;
border-left: 4px solid var(--primary-color);
}
.my-account-stat:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.my-account-stat-value {
font-size: 32px;
font-weight: bold;
color: var(--primary-color);
margin: 10px 0;
}
.my-account-stat-label {
font-size: 14px;
color: #666;
}
.payment-toggle {
display: flex;
align-items: center;
gap: 10px;
margin: 15px 0;
padding: 15px;
background: linear-gradient(135deg, #f8f9fa, white);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--success-color);
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.my-jobs-list {
max-height: 600px;
overflow-y: auto;
margin-top: 15px;
}
.my-job-item {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 15px;
margin-bottom: 15px;
background: linear-gradient(135deg, white, #f8f9fa);
position: relative;
box-shadow: var(--shadow-sm);
transition: all 0.3s;
}
.my-job-item:hover {
transform: translateX(5px);
box-shadow: var(--shadow-md);
}
.my-job-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-weight: bold;
color: var(--secondary-color);
}
.payment-status-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
color: white;
}
.payment-status-badge.paid {
background-color: var(--success-color);
}
.payment-status-badge.unpaid {
background-color: var(--warning-color);
}
/* Firebase cloud sync */
.fb-status-bar {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
padding: 12px 14px; margin-bottom: 15px;
background-color: rgba(0,0,0,0.03);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
}
.fb-badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border-radius: 20px;
font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap;
}
.fb-badge i { font-size: 8px; }
.fb-badge-off { background-color: #95a5a6; }
.fb-badge-ok { background-color: var(--success-color); }
.fb-badge-warn { background-color: var(--warning-color); }
.fb-badge-error { background-color: var(--accent-color); }
#fb-status-text { font-size: 13px; color: var(--text-color); }
.fb-meta-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px; margin-bottom: 15px;
}
.fb-meta-item {
padding: 10px; border: 1px solid var(--border-color);
border-radius: var(--radius-sm); background-color: var(--card-bg);
}
.fb-meta-label { font-size: 11px; text-transform: uppercase; color: #888; letter-spacing: 0.5px; }
.fb-meta-value { font-size: 13px; font-weight: 600; word-break: break-all; }
.fb-button-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.fb-note {
margin-top: 15px; padding: 12px 14px; font-size: 13px; line-height: 1.5;
border-left: 4px solid var(--warning-color);
background-color: rgba(243, 156, 18, 0.08);
border-radius: var(--radius-sm);
}
.fb-rules {
margin: 10px 0 0; padding: 10px; overflow-x: auto;
font-family: monospace; font-size: 11px; line-height: 1.4;
background-color: var(--secondary-color); color: #ecf0f1;
border-radius: var(--radius-sm);
}
body.theme-dark .fb-status-bar,
body.theme-dark .fb-meta-item { background-color: rgba(255,255,255,0.05); }
/* ============ TOP BAR BUTTON STYLES (7 options) ============ */
/* 1. Classic — the original gradient buttons (no overrides needed) */
/* 2. Pills */
body.tabstyle-pills .tabs .tab {
background: #eef2f5;
border-radius: 999px;
border: 2px solid transparent;
padding: 10px 22px;
}
body.tabstyle-pills .tabs .tab:hover {
background: #e2e8ee;
transform: none;
}
body.tabstyle-pills .tabs .tab.active {
background: var(--primary-color);
border-color: var(--primary-color);
color: #fff;
box-shadow: var(--shadow-sm);
}
/* 3. Underline */
body.tabstyle-underline .tabs {
gap: 4px;
padding: 6px 10px 0;
}
body.tabstyle-underline .tabs .tab {
background: none;
border: none;
border-bottom: 3px solid transparent;
border-radius: 0;
padding: 12px 16px;
color: #6b7785;
}
body.tabstyle-underline .tabs .tab:hover {
background: none;
color: var(--secondary-color);
border-bottom-color: var(--border-color);
transform: none;
}
body.tabstyle-underline .tabs .tab.active {
background: none;
color: var(--primary-color);
border-bottom-color: var(--primary-color);
box-shadow: none;
}
/* 4. Segmented */
body.tabstyle-segmented .tabs {
gap: 0;
padding: 4px;
background: #eef2f5;
border: 1px solid var(--border-color);
}
body.tabstyle-segmented .tabs .tab {
background: none;
border: none;
border-right: 1px solid rgba(0,0,0,0.08);
border-radius: 0;
padding: 10px 18px;
color: var(--secondary-color);
}
body.tabstyle-segmented .tabs .tab:last-child { border-right: none; }
body.tabstyle-segmented .tabs .tab:hover {
background: rgba(255,255,255,0.7);
transform: none;
}
body.tabstyle-segmented .tabs .tab.active {
background: #fff;
color: var(--primary-color);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
/* 5. Outlined */
body.tabstyle-outlined .tabs .tab {
background: none;
border: 2px solid var(--border-color);
color: var(--secondary-color);
}
body.tabstyle-outlined .tabs .tab:hover {
background: none;
border-color: var(--primary-color);
color: var(--primary-color);
transform: none;
}
body.tabstyle-outlined .tabs .tab.active {
background: none;
border-color: var(--primary-color);
color: var(--primary-color);
box-shadow: inset 0 0 0 1px var(--primary-color);
}
/* 6. Elevated cards */
body.tabstyle-elevated .tabs {
gap: 10px;
background: transparent;
box-shadow: none;
padding: 6px 0;
}
body.tabstyle-elevated .tabs .tab {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
padding: 14px 20px;
}
body.tabstyle-elevated .tabs .tab:hover {
background: var(--card-bg);
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
body.tabstyle-elevated .tabs .tab.active {
background: var(--card-bg);
color: var(--primary-color);
border-color: var(--primary-color);
border-top: 3px solid var(--primary-color);
box-shadow: var(--shadow-md);
}
/* 7. Minimal */
body.tabstyle-minimal .tabs {
background: transparent;
box-shadow: none;
gap: 18px;
padding: 4px 0;
}
body.tabstyle-minimal .tabs .tab {
background: none;
border: none;
border-radius: 0;
padding: 6px 2px;
font-weight: 500;
color: #7b8794;
}
body.tabstyle-minimal .tabs .tab:hover {
background: none;
color: var(--secondary-color);
transform: none;
}
body.tabstyle-minimal .tabs .tab.active {
background: none;
color: var(--primary-color);
font-weight: 700;
box-shadow: none;
}
/* Dark-theme adjustments for the light-surface styles */
body.theme-dark.tabstyle-pills .tabs .tab,
body.theme-dark.tabstyle-segmented .tabs .tab { color: #ecf0f1; }
body.theme-dark.tabstyle-pills .tabs .tab { background: rgba(255,255,255,0.08); }
body.theme-dark.tabstyle-segmented .tabs { background: rgba(255,255,255,0.06); }
body.theme-dark.tabstyle-segmented .tabs .tab.active { background: rgba(255,255,255,0.16); }
body.theme-dark.tabstyle-underline .tabs .tab,
body.theme-dark.tabstyle-minimal .tabs .tab,
body.theme-dark.tabstyle-outlined .tabs .tab { color: #b9c4cf; }
/* ---- Settings picker for the styles ---- */
.tab-style-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
margin-top: 15px;
}
.tab-style-option {
padding: 14px;
border: 3px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: center;
transition: all 0.3s;
background: white;
}
.tab-style-option:hover { background: linear-gradient(135deg, #f0f8ff, white); }
.tab-style-option.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #e3f2fd, #ffffff);
}
.tab-style-option h4 { margin: 0 0 4px; font-size: 14px; }
.tab-style-option p { margin: 0; font-size: 11px; color: #777; }
.tab-style-preview {
display: flex;
justify-content: center;
align-items: flex-end;
gap: 4px;
margin: 10px 0 8px;
padding: 10px 8px;
min-height: 52px;
background: #f8f9fa;
border-radius: var(--radius-sm);
}
.tab-style-preview span {
font-size: 10px;
font-weight: 600;
padding: 6px 10px;
white-space: nowrap;
color: #6b7785;
}
/* preview: classic */
.tab-style-preview.classic span { background: linear-gradient(135deg,#eee,#f5f5f5); border-radius: var(--radius-sm); }
.tab-style-preview.classic span.on { background: linear-gradient(135deg, var(--primary-color), #2980b9); color:#fff; }
/* preview: pills */
.tab-style-preview.pills span { background:#eef2f5; border-radius:999px; }
.tab-style-preview.pills span.on { background: var(--primary-color); color:#fff; }
/* preview: underline */
.tab-style-preview.underline span { border-bottom:3px solid transparent; }
.tab-style-preview.underline span.on { color: var(--primary-color); border-bottom-color: var(--primary-color); }
/* preview: segmented */
.tab-style-preview.segmented { gap:0; align-items:center; }
.tab-style-preview.segmented span { background:#eef2f5; border-right:1px solid rgba(0,0,0,0.1); }
.tab-style-preview.segmented span:last-child { border-right:none; }
.tab-style-preview.segmented span.on { background:#fff; color: var(--primary-color); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm); }
/* preview: outlined */
.tab-style-preview.outlined span { border:2px solid var(--border-color); border-radius: var(--radius-sm); }
.tab-style-preview.outlined span.on { border-color: var(--primary-color); color: var(--primary-color); }
/* preview: elevated */
.tab-style-preview.elevated span { background:#fff; border:1px solid var(--border-color); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); }
.tab-style-preview.elevated span.on { color: var(--primary-color); border-color: var(--primary-color); border-top:3px solid var(--primary-color); }
/* preview: minimal */
.tab-style-preview.minimal { gap:12px; }
.tab-style-preview.minimal span { padding:6px 2px; }
.tab-style-preview.minimal span.on { color: var(--primary-color); font-weight:700; }
/* ==========================================================================
APP SHELL REDESIGN — "Track Rail"
A field tool for a garage-door business: steel deck, graphite ink,
one signal colour borrowed from the trade's own caution stripes.
Appended last so it wins on equal specificity. No markup changes.
========================================================================== */
:root {
/* Steel ramp — cool on purpose, this is powder-coated hardware, not paper */
--ink: #101418;
--zinc-900: #1C2229;
--zinc-700: #3C4653;
--zinc-600: #5A6673;
--zinc-400: #8D99A6;
--zinc-300: #B8C2CC;
--zinc-200: #D9DFE5;
--zinc-100: #E7EBEF;
--zinc-050: #F3F5F7;
--panel: #FFFFFF;
/* Signal — the caution-stripe amber off a bay door warning light */
--signal: #F5A524;
--signal-dark: #D98A0B;
--signal-wash: #FEF4E2;
--brick: #B23B2E;
--go: #2F9E68;
--go-wash:#E8F5EE;
/* Re-point the legacy tokens so every existing rule inherits the system */
--primary-color: var(--ink);
--secondary-color: var(--zinc-900);
--accent-color: var(--brick);
--success-color: var(--go);
--warning-color: var(--signal-dark);
--light-bg: var(--zinc-100);
--card-bg: var(--panel);
--text-color: var(--ink);
--border-color: var(--zinc-200);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
/* Shadows stay nearly invisible — hairlines carry the structure instead */
--shadow-sm: 0 1px 2px rgba(16, 20, 24, 0.06);
--shadow-md: 0 2px 8px rgba(16, 20, 24, 0.08);
--shadow-lg: 0 12px 32px rgba(16, 20, 24, 0.16);
--font-ui: 'Barlow Semi Condensed', 'Segoe UI Semibold', system-ui, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-data: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--bar-h: 56px;
}
/* ---------- base ---------- */
body {
background: var(--zinc-100);
font-family: var(--font-body);
font-size: 15px;
line-height: 1.5;
color: var(--ink);
padding: 0 0 40px;
-webkit-font-smoothing: antialiased;
}
h2, h3, h4 {
font-family: var(--font-ui);
font-weight: 600;
letter-spacing: 0.01em;
color: var(--ink);
}
h2 { font-size: 21px; }
h3 { font-size: 17px; }
h4 { font-size: 15px; }
/* ---------- app bar ---------- */
/* Seam lines echo the horizontal sections of a garage door. */
body h1#main-title {
position: sticky;
top: 0;
z-index: 900;
display: flex;
align-items: center;
gap: 12px;
height: var(--bar-h);
margin: 0 0 0;
padding: 0 18px;
background:
repeating-linear-gradient(
to bottom,
transparent 0 17px,
rgba(255,255,255,0.035) 17px 18px
),
var(--ink);
border: none;
border-radius: 0;
box-shadow: 0 1px 0 rgba(0,0,0,0.25);
font-family: var(--font-ui);
font-size: 19px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #fff;
text-shadow: none;
animation: none;
}
body h1#main-title i {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 7px;
background: var(--signal);
color: var(--ink);
font-size: 15px;
flex-shrink: 0;
}
/* ---------- the track rail (signature) ---------- */
/* Doors run on tracks. The strip is a rail; the active tab is the roller. */
body .tabs {
position: sticky;
top: var(--bar-h);
z-index: 880;
margin: 0 0 20px;
padding: 0 12px;
gap: 0;
background: var(--panel);
border-radius: 0;
border-bottom: 3px solid var(--zinc-200);
box-shadow: var(--shadow-sm);
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
body .tabs::-webkit-scrollbar { display: none; }
body .tabs.grid,
body .tabs.vertical { overflow: visible; flex-wrap: wrap; }
body .tabs.compact { flex-wrap: wrap; }
body .tabs .tab {
position: relative;
flex: 0 0 auto;
padding: 15px 16px;
background: none;
border: none;
border-radius: 0;
font-family: var(--font-ui);
font-size: 15px;
font-weight: 500;
letter-spacing: 0.015em;
white-space: nowrap;
color: var(--zinc-600);
transition: color 0.18s ease;
}
body .tabs .tab:hover {
background: none;
transform: none;
color: var(--ink);
}
body .tabs .tab.active {
background: none;
color: var(--ink);
font-weight: 600;
box-shadow: none;
}
/* the roller sitting on the rail */
body .tabs .tab.active::after {
content: '';
position: absolute;
left: 12px;
right: 12px;
bottom: -3px;
height: 3px;
background: var(--signal);
}
body .tabs .tab i { margin-right: 7px; color: var(--zinc-400); }
body .tabs .tab.active i { color: var(--signal-dark); }
body.tabstyle-classic .tabs .tab.active i { color: var(--signal-dark); }
/* ---------- panels ---------- */
body .container {
max-width: 1120px;
margin: 0 auto 18px;
padding: 22px;
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
body .container:hover { transform: none; box-shadow: var(--shadow-sm); }
body .tab-content { padding: 0 16px; }
/* ---------- controls ---------- */
body button {
min-height: 42px;
padding: 0 16px;
background: var(--signal);
color: var(--ink);
border: none;
border-radius: 10px;
font-family: var(--font-ui);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.015em;
box-shadow: none;
transition: background 0.15s ease, transform 0.1s ease;
}
body button:hover { background: var(--signal-dark); transform: none; box-shadow: none; }
body button:active { transform: translateY(1px); }
body button i { margin-right: 7px; }
body .danger-button { background: var(--brick); color: #fff; }
body .danger-button:hover { background: #973025; }
body .success-button { background: var(--go); color: #fff; }
body .success-button:hover { background: #268255; }
body .warning-button { background: var(--zinc-900); color: #fff; }
body .warning-button:hover { background: var(--ink); }
/* Secondary controls read as ghosts so the amber stays meaningful */
body .tab-order-buttons button,
body .service-list-actions button,
body .company-list-actions button,
body .experience-actions button,
body .tech-data-actions button,
body .calendar-nav button {
background: var(--panel);
color: var(--zinc-700);
border: 1px solid var(--zinc-200);
box-shadow: none;
}
body .tab-order-buttons button:hover,
body .service-list-actions button:hover,
body .company-list-actions button:hover,
body .experience-actions button:hover,
body .tech-data-actions button:hover,
body .calendar-nav button:hover {
background: var(--zinc-050);
border-color: var(--zinc-300);
color: var(--ink);
}
body input, body select, body textarea {
min-height: 44px;
padding: 11px 13px;
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: 10px;
font-family: var(--font-body);
font-size: 15px;
color: var(--ink);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
body textarea { min-height: 88px; }
body input:focus, body select:focus, body textarea:focus {
border-color: var(--signal-dark);
box-shadow: 0 0 0 3px var(--signal-wash);
outline: none;
}
body input::placeholder, body textarea::placeholder { color: var(--zinc-400); }
body label {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--zinc-600);
}
body .form-group { margin-bottom: 16px; }
:focus-visible { outline: 2px solid var(--signal-dark); outline-offset: 2px; }
/* ---------- numbers read as a ledger ---------- */
body .stat-value,
body .job-total-value,
body .tech-stat-value,
body .my-account-stat-value,
body .lead-cost-metric-value,
body .slider-value,
body .calendar-day-profit,
body .payment-balance,
body .fb-meta-value {
font-family: var(--font-data);
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: var(--ink);
}
body .stat-card,
body .job-total-item,
body .tech-stat,
body .my-account-stat,
body .lead-cost-metric,
body .projection-card {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-sm);
box-shadow: none;
color: var(--ink);
}
body .stat-card:hover,
body .job-total-item:hover,
body .my-account-stat:hover { transform: none; box-shadow: var(--shadow-sm); }
body .stat-icon { color: var(--zinc-300); }
body .stat-card h3,
body .job-total-label,
body .tech-stat-label,
body .my-account-stat-label,
body .fb-meta-label {
font-family: var(--font-ui);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--zinc-600);
}
/* ---------- lists and records ---------- */
body .customer-card, body .tech-card-item, body .partner-card-item,
body .job-item, body .offer-item-card, body .strategy-card,
body .mistake-card, body .my-job-item, body .payment-item,
body .reminder-item, body .experience-item, body .tech-data-item,
body .service-list-item, body .company-list-item {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-left: 3px solid var(--zinc-200);
border-radius: var(--radius-sm);
box-shadow: none;
}
body .customer-card:hover, body .tech-card-item:hover,
body .partner-card-item:hover, body .job-item:hover,
body .my-job-item:hover, body .reminder-item:hover {
transform: none;
border-left-color: var(--signal);
box-shadow: var(--shadow-sm);
}
body .payment-status, body .payment-status-badge,
body .company-default-badge, body .reminder-type, body .fb-badge {
font-family: var(--font-ui);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
border-radius: 5px;
}
body .payment-status.paid, body .payment-status-badge.paid {
background: var(--go-wash); color: #1E6B45;
}
body .payment-status.pending, body .payment-status-badge.unpaid {
background: var(--signal-wash); color: #8A5606;
}
/* ---------- tables ---------- */
body table {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-sm);
box-shadow: none;
font-size: 14px;
}
body th {
position: sticky;
top: calc(var(--bar-h) + 52px);
background: var(--zinc-050);
border-bottom: 1px solid var(--zinc-200);
font-family: var(--font-ui);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--zinc-600);
text-shadow: none;
}
body th:hover { background: var(--zinc-100); color: var(--ink); }
body td { border-bottom: 1px solid var(--zinc-100); font-variant-numeric: tabular-nums; }
body tr:hover { background: var(--signal-wash); transform: none; }
/* ---------- collapsible sections ---------- */
body .compact-header, body .settings-section-header, body .sheet-header {
background: var(--zinc-050);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-sm);
font-family: var(--font-ui);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink);
box-shadow: none;
}
body .compact-header:hover, body .settings-section-header:hover,
body .sheet-header:hover { background: var(--zinc-100); }
body .compact-header.active, body .settings-section-header.active {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
body .compact-content, body .settings-section-content, body .sheet-content {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-top: none;
}
body .settings-section { border: none; box-shadow: none; }
/* ---------- selectable option cards ---------- */
body .theme-option, body .layout-option, body .size-option,
body .tab-layout-option, body .title-display-option,
body .tab-style-option, body .service-option {
background: var(--panel);
border: 2px solid var(--zinc-200);
border-radius: var(--radius-sm);
box-shadow: none;
}
body .theme-option:hover, body .layout-option:hover, body .size-option:hover,
body .tab-layout-option:hover, body .title-display-option:hover,
body .tab-style-option:hover, body .service-option:hover {
background: var(--zinc-050);
border-color: var(--zinc-300);
}
body .theme-option.active, body .layout-option.active, body .size-option.active,
body .tab-layout-option.active, body .title-display-option.active,
body .tab-style-option.active, body .service-option.active {
background: var(--signal-wash);
border-color: var(--signal);
}
body .lead-filter button, body .date-range-filter button,
body .time-preset-button {
min-height: 38px;
background: var(--panel);
color: var(--zinc-700);
border: 1px solid var(--zinc-200);
border-radius: 999px;
font-size: 14px;
}
body .lead-filter button.active, body .time-preset-button.active {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
/* ---------- calendar ---------- */
body .calendar-container { border: 1px solid var(--zinc-200); box-shadow: none; }
body .calendar-header {
background: var(--ink);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
font-family: var(--font-ui);
}
body .calendar-day-header {
background: var(--zinc-050);
font-family: var(--font-ui);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--zinc-600);
}
body .calendar-day {
background: var(--panel);
border: 1px solid var(--zinc-100);
border-radius: 6px;
box-shadow: none;
}
body .calendar-day:hover { background: var(--zinc-050); transform: none; }
body .calendar-day.today {
border: 2px solid var(--signal);
background: var(--signal-wash);
}
body .calendar-day-number { font-family: var(--font-data); font-weight: 600; }
body .calendar-job { border-radius: 4px; font-size: 11px; }
/* ---------- modals: sheet on phones, card on desktop ---------- */
body .modal { background: rgba(16, 20, 24, 0.55); backdrop-filter: blur(2px); }
body .modal-content {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
body .close { color: var(--zinc-400); }
body .close:hover, body .close:focus { color: var(--ink); }
/* ---------- misc surfaces ---------- */
body .chart-container {
background: var(--panel);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-sm);
box-shadow: none;
}
body .lead-display, body .profit-display,
body .offer-display, body .projection-display {
background: var(--zinc-050);
border: 1px solid var(--zinc-200);
border-left: 3px solid var(--signal);
border-radius: var(--radius-sm);
font-family: var(--font-body);
}
body .commission-slider { background: var(--zinc-200); border-radius: 999px; height: 6px; }
body .commission-slider::-webkit-slider-thumb {
background: var(--ink); border: 3px solid var(--signal); box-shadow: none;
}
body .commission-slider::-moz-range-thumb {
background: var(--ink); border: 3px solid var(--signal); box-shadow: none;
}
body .toggle-slider { background: var(--zinc-300); }
body input:checked + .toggle-slider { background: var(--go); }
body .fb-status-bar {
background: var(--zinc-050);
border: 1px solid var(--zinc-200);
border-radius: var(--radius-sm);
}
body .fb-badge-ok { background: var(--go); }
body .fb-badge-warn { background: var(--signal-dark); }
body .fb-badge-error { background: var(--brick); }
body .fb-note { border-left-color: var(--signal); background: var(--signal-wash); }
body .fb-rules { background: var(--ink); }
body ::-webkit-scrollbar { width: 10px; height: 10px; }
body ::-webkit-scrollbar-track { background: var(--zinc-100); }
body ::-webkit-scrollbar-thumb { background: var(--zinc-300); border-radius: 999px; }
body ::-webkit-scrollbar-thumb:hover { background: var(--zinc-400); }
/* ---------- themes keep working: each re-points the signal ---------- */
body.theme-green { --signal: #43A047; --signal-dark: #2E7D32; --signal-wash: #E8F5E9;
--primary-color: var(--ink); --text-color: var(--ink); --border-color: var(--zinc-200); }
body.theme-purple { --signal: #8E44AD; --signal-dark: #6C3483; --signal-wash: #F3E9F7;
--primary-color: var(--ink); --text-color: var(--ink); --border-color: var(--zinc-200); }
body.theme-ocean { --signal: #0288D1; --signal-dark: #01669B; --signal-wash: #E3F2FD;
--primary-color: var(--ink); --text-color: var(--ink); --border-color: var(--zinc-200); }
body.theme-green button, body.theme-purple button, body.theme-ocean button { color: #fff; }
body.theme-dark {
--ink: #E8ECF0;
--panel: #191E24;
--zinc-050: #1F252C;
--zinc-100: #12161A;
--zinc-200: #2C343D;
--zinc-300: #3D4753;
--zinc-400: #6B7785;
--zinc-600: #9AA6B2;
--zinc-700: #C2CCD6;
--signal-wash: rgba(245, 165, 36, 0.14);
/* var() in a :root declaration is substituted at :root, so these
legacy tokens must be restated with literals to follow the theme */
--primary-color: #E8ECF0;
--secondary-color: #C2CCD6;
--text-color: #E8ECF0;
--card-bg: #191E24;
--light-bg: #12161A;
--border-color: #2C343D;
background: #12161A;
color: #E8ECF0;
}
body.theme-dark h1#main-title { background: #0B0E11; }
body.theme-dark .tabs { background: #191E24; }
body.theme-dark button { color: #101418; }
body.theme-dark .danger-button,
body.theme-dark .success-button,
body.theme-dark .warning-button { color: #fff; }
body.theme-dark .compact-header.active,
body.theme-dark .settings-section-header.active { background: #2C343D; color: #E8ECF0; }
body.theme-dark .fb-rules { background: #0B0E11; }
/* ---------- responsive: the phone is the primary device ---------- */
@media (max-width: 768px) {
body { font-size: 15px; }
body h1#main-title { font-size: 16px; padding: 0 14px; }
body .tab-content { padding: 0 10px; }
body .container { padding: 16px; border-radius: var(--radius-sm); }
body .tabs { padding: 0 8px; }
body .tabs .tab { padding: 14px 13px; font-size: 14px; }
body .tabs .tab.active::after { left: 9px; right: 9px; }
body button { width: auto; }
body th { position: static; }
/* Modals become bottom sheets */
body .modal-content {
position: fixed;
left: 0; right: 0; bottom: 0;
width: 100%;
max-width: none;
max-height: 88vh;
margin: 0;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
border-bottom: none;
animation: sheetUp 0.24s ease;
}
}
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Title Display = Hide: the rail moves up to fill the app bar's slot */
body:has(#main-title[style*="display: none"]) .tabs { top: 0; }
body:has(#main-title[style*="display: none"]) th { top: 52px; }
</style>
</head>
<body data-rsssl=1>
<h1 id="main-title"><i class="fas fa-door-closed"></i> Lead Management</h1>
<div class="tabs" id="mainTabs">
<div class="tab active" onclick="openTab('lead-entry')" data-tab="lead-entry"><i class="fas fa-plus-circle"></i> <span>Lead Entry</span></div>
<div class="tab" onclick="openTab('job-details')" data-tab="job-details"><i class="fas fa-tools"></i> <span>Job Details</span></div>
<div class="tab" onclick="openTab('customer-management')" data-tab="customer-management"><i class="fas fa-users"></i> <span>Customers</span></div>
<div class="tab" onclick="openTab('offers')" data-tab="offers"><i class="fas fa-gift"></i> <span>Future Offers</span></div>
<div class="tab" onclick="openTab('performance')" data-tab="performance"><i class="fas fa-chart-line"></i> <span>Performance</span></div>
<div class="tab" onclick="openTab('projections')" data-tab="projections"><i class="fas fa-chart-bar"></i> <span>Projections</span></div>
<div class="tab" onclick="openTab('charts')" data-tab="charts"><i class="fas fa-chart-pie"></i> <span>Charts</span></div>
<div class="tab" onclick="openTab('technicians')" data-tab="technicians"><i class="fas fa-user-hard-hat"></i> <span>Technicians</span></div>
<div class="tab" onclick="openTab('my-account')" data-tab="my-account"><i class="fas fa-user-circle"></i> <span>My Account</span></div>
<div class="tab" onclick="openTab('strategies')" data-tab="strategies"><i class="fas fa-chess"></i> <span>Sales Strategies</span></div>
<div class="tab" onclick="openTab('mistakes')" data-tab="mistakes"><i class="fas fa-exclamation-triangle"></i> <span>Common Mistakes</span></div>
<div class="tab" onclick="openTab('calendar')" data-tab="calendar"><i class="fas fa-calendar-alt"></i> <span>Calendar</span></div>
<div class="tab" onclick="openTab('data-management')" data-tab="data-management"><i class="fas fa-database"></i> <span>Data Tools</span></div>
<div class="tab" onclick="openTab('partner-management')" data-tab="partner-management"><i class="fas fa-handshake"></i> <span>Partner Management</span></div>
<div class="tab" onclick="openTab('settings')" data-tab="settings"><i class="fas fa-cog"></i> <span>Settings</span></div>
</div>
<!-- Lead Entry Tab -->
<div id="lead-entry" class="tab-content active">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-info-circle"></i> New Lead Information</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<form id="leadForm">
<div class="form-group" data-field="customer-name">
<label for="customer-name"><i class="fas fa-user"></i> Customer Name:</label>
<input type="text" id="customer-name" required>
</div>
<div class="form-group" data-field="customer-address">
<label for="customer-address"><i class="fas fa-map-marker-alt"></i> Address:</label>
<input type="text" id="customer-address" required>
</div>
<div class="form-group" data-field="customer-phone">
<label for="customer-phone"><i class="fas fa-phone"></i> Phone Number:</label>
<input type="tel" id="customer-phone" required>
</div>
<div class="form-group" data-field="customer-email">
<label for="customer-email"><i class="fas fa-envelope"></i> Email:</label>
<input type="email" id="customer-email">
</div>
<div class="form-group" data-field="company">
<label for="company"><i class="fas fa-building"></i> Company:</label>
<select id="company">
<!-- Companies will be populated by JavaScript -->
</select>
</div>
<div class="form-group" data-field="service-date">
<label for="service-date"><i class="fas fa-calendar-alt"></i> Date:</label>
<input type="date" id="service-date" required>
</div>
<div class="form-group" data-field="service-time">
<label for="service-time"><i class="fas fa-clock"></i> Time Window:</label>
<div class="time-window-container">
<select id="start-time">
<option value="8:00">8:00 AM</option>
<option value="9:00">9:00 AM</option>
<option value="10:00">10:00 AM</option>
<option value="11:00">11:00 AM</option>
<option value="12:00">12:00 PM</option>
<option value="13:00">1:00 PM</option>
<option value="14:00">2:00 PM</option>
<option value="15:00">3:00 PM</option>
<option value="16:00" selected>4:00 PM</option>
<option value="17:00">5:00 PM</option>
<option value="18:00">6:00 PM</option>
</select>
<span class="time-window-separator">to</span>
<select id="end-time">
<option value="9:00">9:00 AM</option>
<option value="10:00">10:00 AM</option>
<option value="11:00">11:00 AM</option>
<option value="12:00">12:00 PM</option>
<option value="13:00">1:00 PM</option>
<option value="14:00">2:00 PM</option>
<option value="15:00">3:00 PM</option>
<option value="16:00">4:00 PM</option>
<option value="17:00" selected>5:00 PM</option>
<option value="18:00">6:00 PM</option>
<option value="19:00">7:00 PM</option>
</select>
</div>
<div class="time-preset-buttons">
<div class="time-preset-button" onclick="setTimeWindow('8:00', '9:00')">8-9 AM</div>
<div class="time-preset-button" onclick="setTimeWindow('9:00', '10:00')">9-10 AM</div>
<div class="time-preset-button" onclick="setTimeWindow('10:00', '11:00')">10-11 AM</div>
<div class="time-preset-button" onclick="setTimeWindow('13:00', '14:00')">1-2 PM</div>
<div class="time-preset-button" onclick="setTimeWindow('14:00', '15:00')">2-3 PM</div>
<div class="time-preset-button active" onclick="setTimeWindow('16:00', '17:00')">4-5 PM</div>
<div class="time-preset-button" onclick="setTimeWindow('17:00', '18:00')">5-6 PM</div>
</div>
</div>
<div class="form-group" data-field="service-notes">
<label for="service-notes"><i class="fas fa-clipboard-list"></i> Service Needed:</label>
<div class="service-options-container">
<div class="service-option active" onclick="selectServiceOption('free-estimate')">Free Estimate</div>
<div class="service-option" onclick="selectServiceOption('custom')">Custom Service</div>
</div>
<div id="custom-service-input" class="custom-service-input">
<input type="text" id="new-service-input" placeholder="Enter custom service">
<button type="button" onclick="addCustomService()"><i class="fas fa-plus"></i></button>
</div>
<div id="service-list-container" class="service-list-container" style="display: none;">
<!-- Custom services will appear here -->
</div>
<textarea id="service-notes" rows="3" placeholder="Describe the service needed"></textarea>
</div>
<div class="form-group" data-field="service-pricing">
<label for="service-pricing"><i class="fas fa-dollar-sign"></i> Pricing Notes:</label>
<textarea id="service-pricing" rows="2" placeholder="e.g., Springs start at $125 per spring"></textarea>
</div>
<div class="form-group" data-field="marketing-source">
<label for="marketing-source"><i class="fas fa-ad"></i> Marketing Source:</label>
<select id="marketing-source">
<option value="Yelp Ads">Yelp Ads</option>
<option value="Facebook">Facebook</option>
<option value="Google Ads">Google Ads</option>
<option value="Direct Mail">Direct Mail</option>
<option value="Referral">Referral</option>
<option value="Organic">Organic</option>
<option value="Affiliate">Affiliate</option>
<option value="Other">Other</option>
</select>
</div>
<button type="button" onclick="generateLead()"><i class="fas fa-file-alt"></i> <span>Generate Lead</span></button>
<button type="button" onclick="saveLead()"><i class="fas fa-save"></i> <span>Save Lead</span></button>
<button type="reset" onclick="clearForm()"><i class="fas fa-eraser"></i> <span>Clear Form</span></button>
</form>
</div>
</div>
<div id="leadOutput" class="lead-display" style="display:none;">
<!-- Lead information will appear here -->
</div>
<div class="share-buttons" id="shareButtons" style="display:none;">
<button onclick="shareViaWhatsApp()"><i class="fab fa-whatsapp"></i> <span>WhatsApp</span></button>
<button onclick="copyToClipboard()"><i class="fas fa-copy"></i> <span>Clipboard</span></button>
<button onclick="shareViaEmail()"><i class="fas fa-envelope"></i> <span>Email</span></button>
<button onclick="addToGoogleCalendar()"><i class="fab fa-google"></i> <span>Add to Google Calendar</span></button>
</div>
</div>
</div>
<!-- Job Details Tab -->
<div id="job-details" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-tools"></i> Job Completion Details</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="lead-filter">
<button class="active" onclick="filterLeads('all')"><i class="fas fa-list"></i> <span>All Leads</span></button>
<button onclick="filterLeads('new')"><i class="fas fa-star"></i> <span>New</span></button>
<button onclick="filterLeads('progress')"><i class="fas fa-spinner"></i> <span>In Progress</span></button>
<button onclick="filterLeads('completed')"><i class="fas fa-check-circle"></i> <span>Completed</span></button>
</div>
<form id="jobForm">
<div class="form-group" data-job-field="select-lead">
<label for="select-lead"><i class="fas fa-search"></i> Select Lead:</label>
<select id="select-lead" onchange="loadLeadDetails()">
<option value="">-- Select a saved lead --</option>
</select>
</div>
<div class="form-group" data-job-field="lead-status-job">
<label for="lead-status-job"><i class="fas fa-tasks"></i> Lead Status:</label>
<select id="lead-status-job">
<option value="New">New</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
<option value="Lost">Lost</option>
</select>
</div>
<div class="form-group" data-job-field="technician-name">
<label for="technician-name"><i class="fas fa-user-hard-hat"></i> Technician:</label>
<select id="technician-name">
<option value="">-- Select Technician --</option>
</select>
</div>
<div class="form-group" data-job-field="payment-type">
<label><i class="fas fa-calculator"></i> Technician Payment Type:</label>
<div style="display: flex; gap: 10px; align-items: center;">
<label style="display: flex; align-items: center; margin-bottom: 0;">
<input type="radio" name="payment-type" value="percentage" checked onchange="togglePaymentType()" style="width: auto; margin-right: 5px;">
Percentage
</label>
<label style="display: flex; align-items: center; margin-bottom: 0;">
<input type="radio" name="payment-type" value="flat" onchange="togglePaymentType()" style="width: auto; margin-right: 5px;">
Flat Rate
</label>
</div>
</div>
<div id="percentage-section" class="partner-section" data-job-field="technician-commission">
<div class="form-group slider-container">
<label for="technician-commission"><i class="fas fa-user-cog"></i> Technician Commission (%):</label>
<input type="range" id="technician-commission" min="0" max="50" value="40" class="commission-slider" oninput="updateCommissionValue()">
<div class="slider-value" id="commission-value">40%</div>
</div>
</div>
<div id="flat-rate-section" class="partner-section" style="display: none;" data-job-field="flat-rate-amount">
<div class="form-group">
<label for="flat-rate-amount"><i class="fas fa-dollar-sign"></i> Flat Rate Amount ($):</label>
<input type="number" id="flat-rate-amount" step="0.01" min="0">
</div>
</div>
<div class="partner-section" data-job-field="partner-section">
<h4><i class="fas fa-handshake"></i> Partner Commission</h4>
<div class="form-group">
<label for="partner-name"><i class="fas fa-user-tie"></i> Partner Name:</label>
<select id="partner-name" onchange="updatePartnerCommissionFromSelect()">
<option value="">-- No Partner --</option>
</select>
</div>
<div class="form-group slider-container">
<label for="partner-commission"><i class="fas fa-hand-holding-usd"></i> Partner Commission (%):</label>
<input type="range" id="partner-commission" min="0" max="50" value="50" class="commission-slider" oninput="updatePartnerCommissionValue()">
<div class="slider-value" id="partner-commission-value">50%</div>
</div>
</div>
<div class="form-group" data-job-field="job-income">
<label for="job-income"><i class="fas fa-dollar-sign"></i> Total Income ($):</label>
<input type="number" id="job-income" step="0.01">
</div>
<div class="form-group" data-job-field="payment-method">
<label for="payment-method"><i class="fas fa-credit-card"></i> Payment Method:</label>
<select id="payment-method">
<option value="Cash">Cash</option>
<option value="Credit Card">Credit Card</option>
<option value="Check">Check</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group" data-job-field="parts-cost">
<label for="parts-cost"><i class="fas fa-box-open"></i> Parts Cost ($):</label>
<input type="number" id="parts-cost" step="0.01">
</div>
<div class="form-group" data-job-field="parts-list">
<label for="parts-list"><i class="fas fa-list"></i> Parts Used:</label>
<textarea id="parts-list" rows="3" placeholder="List parts and costs (e.g., 2 springs @ $125 each)"></textarea>
</div>
<div class="form-group" data-job-field="job-duration">
<label for="job-duration"><i class="fas fa-clock"></i> Job Duration (hours):</label>
<input type="number" id="job-duration" step="0.1">
</div>
<div class="form-group" data-job-field="sale-strategy">
<label for="sale-strategy"><i class="fas fa-chess"></i> Sales Strategy:</label>
<select id="sale-strategy">
<option value="1">1 - Basic</option>
<option value="2">2 - Standard</option>
<option value="3">3 - Enhanced</option>
<option value="4">4 - Premium</option>
<option value="5">5 - Aggressive</option>
<option value="6">6 - Double (10x)</option>
</select>
</div>
<div class="form-group" data-job-field="job-difficulty">
<label for="job-difficulty"><i class="fas fa-tachometer-alt"></i> Job Difficulty (1-5):</label>
<select id="job-difficulty">
<option value="1">1 - Very Easy</option>
<option value="2">2 - Easy</option>
<option value="3">3 - Moderate</option>
<option value="4">4 - Difficult</option>
<option value="5">5 - Very Difficult</option>
</select>
</div>
<div class="form-group" data-job-field="customer-satisfaction">
<label for="customer-satisfaction"><i class="fas fa-smile"></i> Customer Satisfaction (1-5):</label>
<select id="customer-satisfaction">
<option value="1">1 - Very Dissatisfied</option>
<option value="2">2 - Dissatisfied</option>
<option value="3">3 - Neutral</option>
<option value="4">4 - Satisfied</option>
<option value="5">5 - Very Satisfied</option>
</select>
</div>
<div class="form-group" data-job-field="left-review">
<label for="left-review"><i class="fas fa-comment"></i> Customer Left Review?</label>
<select id="left-review">
<option value="No">No</option>
<option value="Yes">Yes</option>
</select>
</div>
<button type="button" onclick="saveJobDetails()"><i class="fas fa-save"></i> <span>Save Job Details</span></button>
<button type="button" onclick="calculateProfit()"><i class="fas fa-calculator"></i> <span>Calculate Profit</span></button>
</form>
<div id="profitCalculation" style="margin-top: 15px; display: none;">
<h3><i class="fas fa-money-bill-wave"></i> Profit Breakdown</h3>
<div id="profitOutput" class="profit-display"></div>
<div class="profit-share-buttons">
<button onclick="shareProfitViaWhatsApp()"><i class="fab fa-whatsapp"></i> <span>WhatsApp</span></button>
<button onclick="copyProfitToClipboard()"><i class="fas fa-copy"></i> <span>Clipboard</span></button>
<button onclick="shareProfitViaEmail()"><i class="fas fa-envelope"></i> <span>Email</span></button>
</div>
</div>
<!-- Payment Management Section -->
<div id="paymentManagement" class="payment-section" style="display: none;">
<h3><i class="fas fa-credit-card"></i> Partner Payment Management</h3>
<div class="payment-form">
<div>
<div class="form-group">
<label for="payment-amount"><i class="fas fa-dollar-sign"></i> Payment Amount ($):</label>
<input type="number" id="payment-amount" step="0.01" min="0">
</div>
<div class="form-group">
<label for="payment-date"><i class="fas fa-calendar"></i> Payment Date:</label>
<input type="date" id="payment-date">
</div>
</div>
<div>
<div class="form-group">
<label for="payment-method-partner"><i class="fas fa-credit-card"></i> Payment Method:</label>
<select id="payment-method-partner">
<option value="Cash">Cash</option>
<option value="Check">Check</option>
<option value="Bank Transfer">Bank Transfer</option>
<option value="PayPal">PayPal</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="payment-notes"><i class="fas fa-sticky-note"></i> Notes:</label>
<input type="text" id="payment-notes" placeholder="Payment reference or notes">
</div>
</div>
</div>
<button onclick="addPartnerPayment()"><i class="fas fa-plus"></i> <span>Add Payment</span></button>
<button onclick="markPaymentPaid()"><i class="fas fa-check"></i> <span>Mark as Paid</span></button>
<div class="payment-balance">
<span>Total Commission Owed:</span>
<span id="total-commission-owed">$0.00</span>
</div>
<div class="payment-history">
<h4><i class="fas fa-history"></i> Payment History</h4>
<div id="paymentHistoryContent">
<!-- Payment history will appear here -->
</div>
</div>
</div>
</div>
</div>
<!-- Completed Jobs Section -->
<div class="completed-jobs-section">
<h3><i class="fas fa-clipboard-check"></i> Completed Jobs</h3>
<div class="completed-jobs-list" id="completedJobsList">
<!-- Completed jobs will appear here -->
</div>
</div>
</div>
</div>
<!-- Customer Management Tab -->
<div id="customer-management" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-users"></i> Customer Management</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="customer-search"><i class="fas fa-search"></i> Search Customers:</label>
<input type="text" id="customer-search" placeholder="Search by name, phone, or address" oninput="searchCustomers()">
</div>
<div class="customer-list" id="customerList">
<!-- Customer cards will appear here -->
</div>
<div id="customerDetails" style="display: none; margin-top: 15px;">
<div class="customer-sheet">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-user"></i> Customer Details</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div class="sheet-row">
<div class="sheet-label">Status:</div>
<div class="sheet-value">
<select id="customer-status" onchange="updateCustomerStatus()">
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="VIP">VIP</option>
<option value="Do Not Contact">Do Not Contact</option>
</select>
</div>
</div>
<div id="customerDetailContent"></div>
</div>
</div>
<div class="job-history">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-history"></i> Job History</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="jobHistoryContent"></div>
</div>
</div>
<div class="offer-history">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-gifts"></i> Offer History</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="offerHistoryContent"></div>
</div>
</div>
<div class="offer-buttons" style="margin-top: 15px;">
<button onclick="prepareOffer()"><i class="fas fa-gift"></i> <span>Prepare Future Offer</span></button>
<button onclick="contactCustomer()"><i class="fas fa-phone"></i> <span>Contact Customer</span></button>
<button onclick="editCustomer()"><i class="fas fa-edit"></i> <span>Edit Customer</span></button>
<button class="danger-button" onclick="deleteCustomer()"><i class="fas fa-trash"></i> <span>Delete Customer</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offers Tab -->
<div id="offers" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-gift"></i> Future Offers Management</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="offer-customer"><i class="fas fa-user"></i> Select Customer:</label>
<select id="offer-customer" onchange="loadCustomerForOffer()">
<option value="">-- Select a customer --</option>
</select>
</div>
<div id="offerForm" style="display: none;">
<div class="form-group">
<label for="offer-subject"><i class="fas fa-heading"></i> Offer Subject:</label>
<input type="text" id="offer-subject" placeholder="e.g., Special Offer on Garage Door Maintenance">
</div>
<div class="form-group">
<label><i class="fas fa-list-check"></i> Select Offers to Include:</label>
<div class="offer-select-custom">
<div class="offer-dropdown">
<button type="button" class="offer-dropdown-btn" onclick="toggleOfferDropdown()">
<span id="selected-offers-text">Select services...</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="offer-dropdown-content" id="offerDropdownContent">
<!-- Offer categories and items will be dynamically generated here -->
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="offer-message"><i class="fas fa-comment-dots"></i> Custom Message:</label>
<textarea id="offer-message" rows="4" placeholder="Add a personalized message for the customer"></textarea>
</div>
<div class="form-group">
<label for="offer-status"><i class="fas fa-tasks"></i> Offer Status:</label>
<select id="offer-status">
<option value="Pending">Pending</option>
<option value="Sent">Sent</option>
<option value="Accepted">Accepted</option>
<option value="Rejected">Rejected</option>
</select>
</div>
<button type="button" onclick="generateOffer()"><i class="fas fa-file-alt"></i> <span>Generate Offer</span></button>
<button type="button" onclick="createNewOffer()"><i class="fas fa-plus-circle"></i> <span>Create New Offer</span></button>
</div>
<div id="offerOutput" class="offer-display" style="display: none; margin-top: 15px;"></div>
<div class="offer-share-buttons" id="offerShareButtons" style="display: none;">
<h3><i class="fas fa-share-alt"></i> Share Offer:</h3>
<button onclick="shareOfferViaWhatsApp()"><i class="fab fa-whatsapp"></i> <span>WhatsApp</span></button>
<button onclick="copyOfferToClipboard()"><i class="fas fa-copy"></i> <span>Clipboard</span></button>
<button onclick="shareOfferViaEmail()"><i class="fas fa-envelope"></i> <span>Email</span></button>
<button onclick="saveOffer()"><i class="fas fa-save"></i> <span>Save Offer</span></button>
</div>
<!-- All Offers Section -->
<div style="margin-top: 30px;">
<h3><i class="fas fa-list"></i> All Offers</h3>
<div id="allOffersList">
<!-- All offers will appear here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Performance Tab -->
<div id="performance" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-chart-line"></i> Business Performance</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-bullseye"></i></div>
<h3>Total Leads</h3>
<div class="stat-value" id="total-leads">0</div>
<p>All leads generated</p>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-percentage"></i></div>
<h3>Conversion Rate</h3>
<div class="stat-value" id="conversion-rate">0%</div>
<p>Leads to jobs</p>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-dollar-sign"></i></div>
<h3>Avg. Job Income</h3>
<div class="stat-value" id="avg-income">$0</div>
<p>Per completed job</p>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-clock"></i></div>
<h3>Avg. Hourly Rate</h3>
<div class="stat-value" id="avg-hourly">$0/hr</div>
<p>Net profit per hour</p>
</div>
</div>
<div class="lead-cost-analysis">
<h3><i class="fas fa-search-dollar"></i> Lead Cost Analysis</h3>
<p>Calculate your marketing ROI by analyzing lead generation costs and conversion rates.</p>
<div class="projection-controls">
<div>
<div class="form-group">
<label for="start-date"><i class="fas fa-calendar-day"></i> Start Date:</label>
<input type="date" id="start-date">
</div>
<div class="form-group">
<label for="end-date"><i class="fas fa-calendar-day"></i> End Date:</label>
<input type="date" id="end-date">
</div>
</div>
<div>
<div class="form-group">
<label for="marketing-spend"><i class="fas fa-money-bill-wave"></i> Total Marketing Spend ($):</label>
<input type="number" id="marketing-spend" step="0.01" onchange="calculateLeadCost()">
</div>
<div class="form-group">
<label for="leads-generated"><i class="fas fa-bullhorn"></i> Leads Generated:</label>
<input type="number" id="leads-generated" onchange="calculateLeadCost()" value="0">
</div>
</div>
</div>
<div class="lead-cost-metrics">
<div class="lead-cost-metric">
<h4><i class="fas fa-dollar-sign"></i> Cost Per Lead</h4>
<div class="lead-cost-metric-value" id="cost-per-lead">$0.00</div>
<p>Marketing spend divided by leads</p>
</div>
<div class="lead-cost-metric">
<h4><i class="fas fa-exchange-alt"></i> Conversion Rate</h4>
<div class="lead-cost-metric-value" id="calculated-conversion-rate">0%</div>
<p>Jobs completed from leads</p>
</div>
</div>
<div class="lead-cost-result">
<i class="fas fa-lightbulb"></i>
<div>
<strong>Insight:</strong> <span id="lead-cost-insight">Enter your marketing data to see insights</span>
</div>
</div>
</div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-history"></i> Recent Jobs</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="date-range-filter">
<div class="form-group">
<label for="job-start-date"><i class="fas fa-calendar-day"></i> From:</label>
<input type="date" id="job-start-date">
</div>
<div class="form-group">
<label for="job-end-date"><i class="fas fa-calendar-day"></i> To:</label>
<input type="date" id="job-end-date">
</div>
<button onclick="filterJobsByDate()"><i class="fas fa-filter"></i> <span>Filter</span></button>
<button onclick="resetJobDateFilter()"><i class="fas fa-undo"></i> <span>Reset</span></button>
</div>
<div class="export-buttons">
<h4><i class="fas fa-download"></i> Export Options</h4>
<button onclick="exportToPDF()"><i class="fas fa-file-pdf"></i> <span>Export to PDF</span></button>
<button onclick="exportToCSV()"><i class="fas fa-file-csv"></i> <span>Export to CSV</span></button>
<button onclick="exportToGoogleSheets()"><i class="fas fa-file-spreadsheet"></i> <span>Google Sheets Format</span></button>
</div>
<div style="overflow-x: auto;">
<table id="jobs-table">
<thead>
<tr>
<th onclick="sortTable(0)">Date <i class="fas fa-sort"></i></th>
<th onclick="sortTable(1)">Customer <i class="fas fa-sort"></i></th>
<th onclick="sortTable(2)">Service <i class="fas fa-sort"></i></th>
<th onclick="sortTable(3)">Income <i class="fas fa-sort"></i></th>
<th onclick="sortTable(4)">Profit <i class="fas fa-sort"></i></th>
<th onclick="sortTable(5)">Satisfaction <i class="fas fa-sort"></i></th>
<th onclick="sortTable(6)">Review <i class="fas fa-sort"></i></th>
<th onclick="sortTable(7)">Duration <i class="fas fa-sort"></i></th>
<th onclick="sortTable(8)">Technician <i class="fas fa-sort"></i></th>
<th onclick="sortTable(9)">Commission <i class="fas fa-sort"></i></th>
<th onclick="sortTable(10)">Parts Cost <i class="fas fa-sort"></i></th>
<th onclick="sortTable(11)">Status <i class="fas fa-sort"></i></th>
</tr>
</thead>
<tbody>
<!-- Job data will appear here -->
</tbody>
</table>
</div>
<div class="job-totals">
<h3><i class="fas fa-calculator"></i> Job Totals</h3>
<div class="job-totals-grid">
<div class="job-total-item">
<div class="job-total-value" id="total-jobs">0</div>
<div class="job-total-label">Total Jobs</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-income">$0</div>
<div class="job-total-label">Total Income</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-gross-profit">$0</div>
<div class="job-total-label">Total Gross Profit</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="avg-sale">$0</div>
<div class="job-total-label">Average Sale</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-duration">0</div>
<div class="job-total-label">Total Hours</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-profit">$0</div>
<div class="job-total-label">Total Net Profit</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="avg-profit">$0</div>
<div class="job-total-label">Avg Net Profit</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="avg-satisfaction">0</div>
<div class="job-total-label">Avg Satisfaction</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-parts">$0</div>
<div class="job-total-label">Total Parts Cost</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="avg-parts">$0</div>
<div class="job-total-label">Avg Parts Cost</div>
</div>
<div class="job-total-item">
<div class="job-total-value" id="total-reviews">0</div>
<div class="job-total-label">Reviews Left</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Projections Tab -->
<div id="projections" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-chart-bar"></i> Business Projections</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="projection-controls">
<div>
<div class="form-group">
<label for="projection-months"><i class="fas fa-calendar-alt"></i> Projection Period (months):</label>
<select id="projection-months">
<option value="3">3 Months</option>
<option value="6">6 Months</option>
<option value="12" selected>12 Months</option>
<option value="24">24 Months</option>
</select>
</div>
<div class="form-group">
<label for="growth-rate"><i class="fas fa-chart-line"></i> Monthly Growth Rate (%):</label>
<input type="number" id="growth-rate" value="5" min="0" max="100" step="1">
</div>
</div>
<div>
<div class="form-group">
<label for="strategy-multiplier"><i class="fas fa-chess-queen"></i> Strategy Multiplier:</label>
<select id="strategy-multiplier">
<option value="1">1x (Current Strategy)</option>
<option value="2">2x</option>
<option value="5">5x</option>
<option value="10">10x (Double Strategy)</option>
</select>
</div>
<div class="form-group">
<label for="commission-rate"><i class="fas fa-percentage"></i> Avg. Commission Rate (%):</label>
<input type="number" id="commission-rate" value="40" min="30" max="50" step="1">
</div>
</div>
</div>
<button onclick="calculateProjections()"><i class="fas fa-calculator"></i> <span>Calculate Projections</span></button>
<div id="projectionResults" style="margin-top: 15px;">
<div class="projection-card">
<h3><i class="fas fa-chart-line"></i> Projected Performance</h3>
<div id="projectionOutput" class="projection-display"></div>
</div>
<div class="chart-container">
<canvas id="projectionChart"></canvas>
</div>
<div class="two-column">
<div class="projection-card">
<h3><i class="fas fa-key"></i> Key Metrics</h3>
<table>
<tr>
<th>Projected Total Income</th>
<td id="projected-income">$0</td>
</tr>
<tr>
<th>Projected Net Profit</th>
<td id="projected-profit">$0</td>
</tr>
<tr>
<th>Projected Jobs</th>
<td id="projected-jobs">0</td>
</tr>
<tr>
<th>Projected ROI</th>
<td id="projected-roi">0%</td>
</tr>
</table>
</div>
<div class="projection-card">
<h3><i class="fas fa-rocket"></i> 10x Strategy Impact</h3>
<table>
<tr>
<th>Additional Income</th>
<td id="additional-income">$0</td>
</tr>
<tr>
<th>Additional Profit</th>
<td id="additional-profit">$0</td>
</tr>
<tr>
<th>Additional Jobs</th>
<td id="additional-jobs">0</td>
</tr>
<tr>
<th>ROI Improvement</th>
<td id="roi-improvement">0%</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Tab -->
<div id="charts" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-chart-pie"></i> Performance Charts</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="two-column">
<div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-money-bill-wave"></i> Monthly Income</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="chart-container">
<canvas id="incomeChart"></canvas>
</div>
</div>
</div>
</div>
<div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-ad"></i> Lead Sources</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="chart-container">
<canvas id="sourceChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="two-column">
<div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-coins"></i> Job Profitability</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="chart-container">
<canvas id="profitChart"></canvas>
</div>
</div>
</div>
</div>
<div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-smile"></i> Customer Satisfaction</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="chart-container">
<canvas id="satisfactionChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div>
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-gift"></i> Offer Performance</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="chart-container">
<canvas id="offerChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Technicians Tab -->
<div id="technicians" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-user-hard-hat"></i> Technician Management</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="new-tech-name"><i class="fas fa-user-plus"></i> Add New Technician:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-tech-name" placeholder="Technician name">
<button onclick="addTechnician()"><i class="fas fa-plus"></i> <span>Add</span></button>
</div>
</div>
<div id="techniciansList" class="customer-list">
<!-- Technician cards will appear here -->
</div>
<div id="technicianDetails" style="display: none; margin-top: 15px;">
<div class="tech-section">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-chart-line"></i> Technician Performance</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="techDetailContent"></div>
</div>
</div>
<!-- NEW: Enhanced Technician Details Section -->
<div class="tech-detail-section">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-clipboard-list"></i> Enhanced Technician Details</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div class="tech-detail-categories">
<div class="tech-detail-category">
<h4><i class="fas fa-id-card"></i> Personal Information</h4>
<div class="tech-detail-field">
<label>Email Address:</label>
<input type="email" id="tech-email" placeholder="[email protected]">
</div>
<div class="tech-detail-field">
<label>Phone Number:</label>
<input type="tel" id="tech-phone" placeholder="(555) 123-4567">
</div>
<div class="tech-detail-field">
<label>Address:</label>
<input type="text" id="tech-address" placeholder="Street address">
</div>
<div class="tech-detail-field">
<label>Emergency Contact:</label>
<input type="text" id="tech-emergency-contact" placeholder="Name and phone">
</div>
</div>
<div class="tech-detail-category">
<h4><i class="fas fa-graduation-cap"></i> Qualifications & Certifications</h4>
<div class="tech-detail-field">
<label>Years of Experience:</label>
<input type="number" id="tech-experience" min="0" max="50" placeholder="5">
</div>
<div class="tech-detail-field">
<label>Certifications:</label>
<textarea id="tech-certifications" rows="3" placeholder="List certifications"></textarea>
</div>
<div class="tech-detail-field">
<label>Specializations:</label>
<textarea id="tech-specializations" rows="3" placeholder="Areas of expertise"></textarea>
</div>
<div class="tech-detail-field">
<label>Training Completed:</label>
<textarea id="tech-training" rows="3" placeholder="Recent training courses"></textarea>
</div>
</div>
<div class="tech-detail-category">
<h4><i class="fas fa-tools"></i> Skills & Equipment</h4>
<div class="tech-detail-field">
<label>Primary Skills:</label>
<textarea id="tech-skills" rows="3" placeholder="Key technical skills"></textarea>
</div>
<div class="tech-detail-field">
<label>Tools & Equipment:</label>
<textarea id="tech-equipment" rows="3" placeholder="Specialized equipment owned"></textarea>
</div>
<div class="tech-detail-field">
<label>Vehicle Information:</label>
<input type="text" id="tech-vehicle" placeholder="Make, model, year">
</div>
<div class="tech-detail-field">
<label>Insurance Status:</label>
<select id="tech-insurance">
<option value="">Select status</option>
<option value="Active">Active</option>
<option value="Expired">Expired</option>
<option value="Not Required">Not Required</option>
</select>
</div>
</div>
<div class="tech-detail-category">
<h4><i class="fas fa-business-time"></i> Availability & Preferences</h4>
<div class="tech-detail-field">
<label>Preferred Work Hours:</label>
<select id="tech-hours">
<option value="">Select preference</option>
<option value="Morning">Morning (8AM-12PM)</option>
<option value="Afternoon">Afternoon (12PM-5PM)</option>
<option value="Evening">Evening (5PM-9PM)</option>
<option value="Flexible">Flexible</option>
</select>
</div>
<div class="tech-detail-field">
<label>Maximum Daily Jobs:</label>
<input type="number" id="tech-max-jobs" min="1" max="10" value="4">
</div>
<div class="tech-detail-field">
<label>Service Area Radius (miles):</label>
<input type="number" id="tech-service-radius" min="5" max="100" value="25">
</div>
<div class="tech-detail-field">
<label>Preferred Job Types:</label>
<textarea id="tech-job-preferences" rows="3" placeholder="Types of jobs preferred"></textarea>
</div>
</div>
<div class="tech-detail-category">
<h4><i class="fas fa-money-bill-wave"></i> Compensation & Performance</h4>
<div class="tech-detail-field">
<label>Base Pay Rate ($/hr):</label>
<input type="number" id="tech-base-rate" step="0.01" min="0" placeholder="25.00">
</div>
<div class="tech-detail-field">
<label>Commission Structure:</label>
<textarea id="tech-commission-structure" rows="3" placeholder="Commission details"></textarea>
</div>
<div class="tech-detail-field">
<label>Performance Goals:</label>
<textarea id="tech-performance-goals" rows="3" placeholder="Monthly/quarterly targets"></textarea>
</div>
<div class="tech-detail-field">
<label>Bonus Eligibility:</label>
<select id="tech-bonus-eligible">
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
</div>
<div class="tech-detail-category">
<h4><i class="fas fa-sticky-note"></i> Additional Notes</h4>
<div class="tech-detail-field">
<label>General Notes:</label>
<textarea id="tech-general-notes" rows="4" placeholder="Any additional information"></textarea>
</div>
<div class="tech-detail-field">
<label>Strengths:</label>
<textarea id="tech-strengths" rows="3" placeholder="Key strengths"></textarea>
</div>
<div class="tech-detail-field">
<label>Areas for Improvement:</label>
<textarea id="tech-improvements" rows="3" placeholder="Development areas"></textarea>
</div>
</div>
</div>
<button onclick="saveTechnicianDetails()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save All Technician Details</span></button>
<button onclick="resetTechnicianDetails()" class="danger-button" style="margin-top: 15px;"><i class="fas fa-undo"></i> <span>Reset Technician Details</span></button>
<button onclick="deleteTechnicianDetails()" class="danger-button" style="margin-top: 15px;"><i class="fas fa-trash"></i> <span>Delete Technician Details</span></button>
</div>
</div>
<div class="job-history">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-history"></i> Job History with Breakdown</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="techJobHistoryContent"></div>
</div>
</div>
<div class="offer-buttons" style="margin-top: 15px;">
<button onclick="editTechnician()"><i class="fas fa-edit"></i> <span>Edit Technician</span></button>
<button class="danger-button" onclick="deleteTechnician()"><i class="fas fa-trash"></i> <span>Delete Technician</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- My Account Tab -->
<div id="my-account" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-user-circle"></i> My Account</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<h2><i class="fas fa-user"></i> My Job Statistics</h2>
<div class="my-account-stats">
<div class="my-account-stat">
<div class="stat-icon"><i class="fas fa-tools"></i></div>
<div class="my-account-stat-value" id="my-total-jobs">0</div>
<div class="my-account-stat-label">Total Jobs</div>
</div>
<div class="my-account-stat">
<div class="stat-icon"><i class="fas fa-dollar-sign"></i></div>
<div class="my-account-stat-value" id="my-total-commission">$0</div>
<div class="my-account-stat-label">Total Commission</div>
</div>
<div class="my-account-stat">
<div class="stat-icon"><i class="fas fa-clock"></i></div>
<div class="my-account-stat-value" id="my-avg-hourly">$0/hr</div>
<div class="my-account-stat-label">Avg Hourly Rate</div>
</div>
<div class="my-account-stat">
<div class="stat-icon"><i class="fas fa-smile"></i></div>
<div class="my-account-stat-value" id="my-avg-satisfaction">0/5</div>
<div class="my-account-stat-label">Avg Satisfaction</div>
</div>
</div>
<div class="payment-toggle">
<span><strong>Show:</strong></span>
<label class="toggle-switch">
<input type="checkbox" id="payment-filter-toggle" onchange="togglePaymentFilter()">
<span class="toggle-slider"></span>
</label>
<span id="payment-filter-label">All Jobs</span>
</div>
<h3><i class="fas fa-history"></i> My Job History</h3>
<div class="my-jobs-list" id="myJobsList">
<!-- My jobs will appear here -->
</div>
</div>
</div>
</div>
</div>
<!-- Sales Strategies Tab -->
<div id="strategies" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-chess"></i> Sales Strategies</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="new-strategy-name"><i class="fas fa-plus"></i> Add New Strategy:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-strategy-name" placeholder="Strategy name">
<button onclick="addStrategy()"><i class="fas fa-plus"></i> <span>Add</span></button>
</div>
</div>
<div class="form-group">
<label for="strategy-description"><i class="fas fa-edit"></i> Strategy Description:</label>
<textarea id="strategy-description" rows="4" placeholder="Describe the strategy in detail"></textarea>
</div>
<div class="form-group">
<label for="strategy-success-rate"><i class="fas fa-percentage"></i> Success Rate (%):</label>
<input type="number" id="strategy-success-rate" min="0" max="100" value="50">
</div>
<button onclick="saveStrategy()"><i class="fas fa-save"></i> <span>Save Strategy</span></button>
<div id="strategiesList" style="margin-top: 20px;">
<!-- Strategy cards will appear here -->
</div>
</div>
</div>
</div>
</div>
<!-- Common Mistakes Tab -->
<div id="mistakes" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-exclamation-triangle"></i> Common Mistakes</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="new-mistake-name"><i class="fas fa-plus"></i> Add New Mistake:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-mistake-name" placeholder="Mistake description">
<button onclick="addMistake()"><i class="fas fa-plus"></i> <span>Add</span></button>
</div>
</div>
<div class="form-group">
<label for="mistake-solution"><i class="fas fa-lightbulb"></i> Solution/Prevention:</label>
<textarea id="mistake-solution" rows="4" placeholder="Describe how to prevent or fix this mistake"></textarea>
</div>
<div class="form-group">
<label for="mistake-frequency"><i class="fas fa-chart-bar"></i> Frequency (1-5):</label>
<select id="mistake-frequency">
<option value="1">1 - Rare</option>
<option value="2">2 - Occasional</option>
<option value="3" selected>3 - Moderate</option>
<option value="4">4 - Frequent</option>
<option value="5">5 - Very Common</option>
</select>
</div>
<button onclick="saveMistake()"><i class="fas fa-save"></i> <span>Save Mistake</span></button>
<div id="mistakesList" style="margin-top: 20px;">
<!-- Mistake cards will appear here -->
</div>
</div>
</div>
</div>
</div>
<!-- Calendar Tab -->
<div id="calendar" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-calendar-alt"></i> Job Calendar & Follow-ups</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="calendar-legend">
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #64b5f6, #42a5f5);"></div>
<span>New</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #ffb74d, #ffa726);"></div>
<span>In Progress</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #81c784, #66bb6a);"></div>
<span>Completed</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #e57373, #ef5350);"></div>
<span>Lost</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #ba68c8, #ab47bc);"></div>
<span>Follow-up</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #26c6da, #00acc1);"></div>
<span>Call Reminder</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #ffa726, #ff9800);"></div>
<span>Email (30-day)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #8bc34a, #7cb342);"></div>
<span>Maintenance (365-day)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #ffca28, #ffc107);"></div>
<span>Review Request</span>
</div>
</div>
<div class="calendar-header">
<div class="calendar-nav">
<button onclick="previousMonth()"><i class="fas fa-chevron-left"></i> <span>Previous</span></button>
<button onclick="currentMonth()"><i class="fas fa-calendar-day"></i> <span>Today</span></button>
<button onclick="nextMonth()"><span>Next</span> <i class="fas fa-chevron-right"></i></button>
</div>
<h2 id="calendar-month-year">Month Year</h2>
<button onclick="addFollowUp()"><i class="fas fa-plus-circle"></i> <span>Add Follow-up</span></button>
</div>
<div class="calendar-container">
<div class="calendar-grid">
<div class="calendar-day-header">Sun</div>
<div class="calendar-day-header">Mon</div>
<div class="calendar-day-header">Tue</div>
<div class="calendar-day-header">Wed</div>
<div class="calendar-day-header">Thu</div>
<div class="calendar-day-header">Fri</div>
<div class="calendar-day-header">Sat</div>
</div>
<div class="calendar-grid" id="calendar-days">
<!-- Calendar days will be generated here -->
</div>
</div>
<!-- Upcoming Reminders Section -->
<div class="upcoming-reminders">
<h3><i class="fas fa-bell"></i> Upcoming Reminders</h3>
<div class="reminder-list" id="upcomingRemindersList">
<!-- Upcoming reminders will appear here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Management Tab -->
<div id="data-management" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-database"></i> Data Management Tools</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="data-import-export">
<h3><i class="fas fa-file-export"></i> Export Data</h3>
<p>Download all your business data as a JSON file for backup or analysis.</p>
<button onclick="exportData()"><i class="fas fa-download"></i> <span>Export All Data</span></button>
<h3 style="margin-top: 20px;"><i class="fas fa-file-import"></i> Import Data</h3>
<p>Restore your business data from a previously exported JSON file.</p>
<div class="form-group">
<label for="data-file"><i class="fas fa-file"></i> Select Data File:</label>
<input type="file" id="data-file" accept=".json">
</div>
<button onclick="importData()"><i class="fas fa-upload"></i> <span>Import Data</span></button>
<div id="import-result" style="margin-top: 10px; font-weight: bold;"></div>
<h3 style="margin-top: 20px;"><i class="fas fa-exclamation-triangle"></i> Data Reset</h3>
<p>Warning: This will permanently delete all your data. Use with caution.</p>
<button class="danger-button" onclick="resetData()"><i class="fas fa-trash"></i> <span>Reset All Data</span></button>
<h3 style="margin-top: 20px;"><i class="fas fa-history"></i> Backup Reminder</h3>
<p>Regularly export your data to prevent any loss of information. We recommend exporting after each work day.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Partner Management Tab -->
<div id="partner-management" class="tab-content">
<div class="container">
<div class="compact-section">
<div class="compact-header active" onclick="toggleCompactSection(this)">
<span><i class="fas fa-handshake"></i> Partner Management</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="compact-content expanded">
<div class="form-group">
<label for="partner-name-input"><i class="fas fa-user-tie"></i> Partner Name:</label>
<input type="text" id="partner-name-input" placeholder="Enter partner name">
</div>
<div class="form-group">
<label for="partner-phone"><i class="fas fa-phone"></i> Partner Phone:</label>
<input type="tel" id="partner-phone" placeholder="Enter partner phone">
</div>
<div class="form-group">
<label for="partner-email"><i class="fas fa-envelope"></i> Partner Email:</label>
<input type="email" id="partner-email" placeholder="Enter partner email">
</div>
<div class="form-group">
<label for="partner-commission-input"><i class="fas fa-percentage"></i> Partner Commission (%):</label>
<input type="number" id="partner-commission-input" min="0" max="100" value="50" step="1">
</div>
<div class="form-group">
<label for="partner-status"><i class="fas fa-tasks"></i> Partner Status:</label>
<select id="partner-status">
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Pending">Pending</option>
</select>
</div>
<button onclick="addPartner()"><i class="fas fa-plus"></i> <span>Add Partner</span></button>
<button onclick="updatePartner()"><i class="fas fa-save"></i> <span>Update Partner</span></button>
<div id="partnersList" style="margin-top: 20px;">
<!-- Partner cards will appear here -->
</div>
<div id="partnerDetails" style="display: none; margin-top: 15px;">
<div class="compact-section">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-chart-line"></i> Partner Performance</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="partnerDetailContent"></div>
</div>
</div>
<div class="partner-experience-section">
<h3><i class="fas fa-star"></i> Work Experience & Reviews</h3>
<button onclick="addPartnerExperience()"><i class="fas fa-plus"></i> <span>Add Experience</span></button>
<div id="partnerExperienceContent" style="margin-top: 15px;">
<!-- Partner experiences will appear here -->
</div>
</div>
<div class="job-history">
<div class="sheet-header active" onclick="toggleSheetSection(this)">
<span><i class="fas fa-history"></i> Job History</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="sheet-content">
<div id="partnerJobHistoryContent"></div>
</div>
</div>
<div class="offer-buttons" style="margin-top: 15px;">
<button onclick="editPartner()"><i class="fas fa-edit"></i> <span>Edit Partner</span></button>
<button class="danger-button" onclick="deletePartner()"><i class="fas fa-trash"></i> <span>Delete Partner</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Tab -->
<div id="settings" class="tab-content">
<div class="container">
<h2><i class="fas fa-cog"></i> Application Settings</h2>
<!-- NEW: Firebase Cloud Sync -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-cloud"></i> Cloud Sync (Firebase)</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Keep leads, jobs, customers and settings in sync across every device you sign in on.</p>
<div class="fb-status-bar">
<span id="fb-status-badge" class="fb-badge fb-badge-off"><i class="fas fa-circle"></i> Not connected</span>
<span id="fb-status-text">Paste your Firebase config below to get started.</span>
</div>
<div class="fb-meta-grid">
<div class="fb-meta-item">
<div class="fb-meta-label">Signed in as</div>
<div class="fb-meta-value" id="fb-user-email">—</div>
</div>
<div class="fb-meta-item">
<div class="fb-meta-label">Last sync</div>
<div class="fb-meta-value" id="fb-last-sync">Never</div>
</div>
<div class="fb-meta-item">
<div class="fb-meta-label">Connection</div>
<div class="fb-meta-value" id="fb-conn-state">Offline</div>
</div>
<div class="fb-meta-item">
<div class="fb-meta-label">Queued changes</div>
<div class="fb-meta-value" id="fb-pending-state">None</div>
</div>
</div>
<div class="form-group">
<label for="fb-config"><strong>Firebase config</strong> (paste the <code>firebaseConfig</code> object from your Firebase console)</label>
<textarea id="fb-config" rows="9" placeholder='{
"apiKey": "AIza...",
"authDomain": "your-project.firebaseapp.com",
"databaseURL": "https://your-project-default-rtdb.firebaseio.com",
"projectId": "your-project",
"appId": "1:123456789:web:abc123"
}' style="font-family: monospace; font-size: 12px;"></textarea>
<small style="color: #666;">Requires Realtime Database. The <code>databaseURL</code> field is mandatory.</small>
</div>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="fb-auto-sync" onchange="fbToggleAutoSync()" style="width: 20px; height: 20px;">
<label for="fb-auto-sync" style="margin: 0;"><strong>Auto-sync</strong> — push changes to the cloud automatically as you work</label>
</div>
</div>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="fb-auto-connect" onchange="fbToggleAutoConnect()" style="width: 20px; height: 20px;">
<label for="fb-auto-connect" style="margin: 0;"><strong>Connect on startup</strong> — reconnect automatically when the app loads</label>
</div>
</div>
<div class="fb-button-row">
<button onclick="fbSaveAndConnect()"><i class="fas fa-plug"></i> <span>Save & Connect</span></button>
<button onclick="fbSignIn()" class="success-button"><i class="fab fa-google"></i> <span>Sign in with Google</span></button>
<button onclick="fbSignOut()"><i class="fas fa-sign-out-alt"></i> <span>Sign Out</span></button>
</div>
<div class="fb-button-row">
<button onclick="fbPushNow()"><i class="fas fa-cloud-upload-alt"></i> <span>Push to Cloud</span></button>
<button onclick="fbPullNow()"><i class="fas fa-cloud-download-alt"></i> <span>Pull from Cloud</span></button>
<button onclick="fbDisconnect()" class="danger-button"><i class="fas fa-unlink"></i> <span>Disconnect</span></button>
</div>
<div class="fb-note">
<strong><i class="fas fa-shield-alt"></i> Lock your database down.</strong>
Your data lives at <code>leadManagement/<your-uid></code>. Paste these rules into
Realtime Database → Rules so only you can read or write your own records:
<pre class="fb-rules">{
"rules": {
"leadManagement": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
}
}
}</pre>
Also enable the Google sign-in provider under Authentication → Sign-in method, and add
this app's domain to the authorised domains list.
</div>
</div>
</div>
<!-- NEW: Reminder Settings -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-bell"></i> Reminder Settings</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Configure automatic reminders for customer follow-ups</p>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enable-reminders" onchange="toggleReminders()" style="width: 20px; height: 20px;">
<label for="enable-reminders" style="margin: 0;"><strong>Enable Automatic Reminders</strong></label>
</div>
</div>
<div id="reminder-options" style="display: none; margin-top: 15px;">
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enable-call-reminder" style="width: 20px; height: 20px;">
<label for="enable-call-reminder" style="margin: 0;">Call customer 1 day after job completion</label>
</div>
</div>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enable-email-reminder" style="width: 20px; height: 20px;">
<label for="enable-email-reminder" style="margin: 0;">Send email 30 days after service</label>
</div>
</div>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enable-maintenance-reminder" style="width: 20px; height: 20px;">
<label for="enable-maintenance-reminder" style="margin: 0;">Annual maintenance reminder (365 days)</label>
</div>
</div>
<div class="form-group">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enable-review-reminder" style="width: 20px; height: 20px;">
<label for="enable-review-reminder" style="margin: 0;">Request review on same day as job (for satisfied customers)</label>
</div>
</div>
</div>
<button onclick="saveReminderSettings()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Reminder Settings</span></button>
</div>
</div>
<!-- NEW: Title Display Setting -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-heading"></i> Title Display</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose whether to show or hide the application title</p>
<div class="title-display-options">
<div class="title-display-option active" data-display="show" onclick="applyTitleDisplay('show')">
<h4>Show Title</h4>
<div class="title-display-preview">
<h1 style="margin: 0; font-size: 18px; padding: 10px 15px;"><i class="fas fa-door-closed"></i> Lead Management</h1>
</div>
</div>
<div class="title-display-option" data-display="hide" onclick="applyTitleDisplay('hide')">
<h4>Hide Title</h4>
<div class="title-display-preview">
<p>Title will be hidden</p>
</div>
</div>
</div>
<button onclick="saveTitleDisplaySetting()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Title Display Setting</span></button>
</div>
</div>
<!-- NEW: Tab Layout Options -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-th"></i> Tab Layout Options</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose how tabs are displayed</p>
<div class="tab-layout-options">
<div class="tab-layout-option active" data-layout="horizontal" onclick="applyTabLayout('horizontal')">
<h4>Horizontal</h4>
<p>Default horizontal layout</p>
<div class="tab-layout-preview horizontal">
<div class="tab-layout-preview-item">Tab 1</div>
<div class="tab-layout-preview-item">Tab 2</div>
<div class="tab-layout-preview-item">Tab 3</div>
</div>
</div>
<div class="tab-layout-option" data-layout="vertical" onclick="applyTabLayout('vertical')">
<h4>Vertical</h4>
<p>Sidebar style layout</p>
<div class="tab-layout-preview vertical">
<div class="tab-layout-preview-item">Tab 1</div>
<div class="tab-layout-preview-item">Tab 2</div>
<div class="tab-layout-preview-item">Tab 3</div>
</div>
</div>
<div class="tab-layout-option" data-layout="grid" onclick="applyTabLayout('grid')">
<h4>Grid</h4>
<p>Grid layout for better organization</p>
<div class="tab-layout-preview grid">
<div class="tab-layout-preview-item">Tab 1</div>
<div class="tab-layout-preview-item">Tab 2</div>
<div class="tab-layout-preview-item">Tab 3</div>
<div class="tab-layout-preview-item">Tab 4</div>
</div>
</div>
<div class="tab-layout-option" data-layout="compact" onclick="applyTabLayout('compact')">
<h4>Compact</h4>
<p>Compact layout with smaller tabs</p>
<div class="tab-layout-preview compact">
<div class="tab-layout-preview-item">Tab 1</div>
<div class="tab-layout-preview-item">Tab 2</div>
<div class="tab-layout-preview-item">Tab 3</div>
<div class="tab-layout-preview-item">Tab 4</div>
</div>
</div>
</div>
<button onclick="saveTabLayoutSetting()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Tab Layout Setting</span></button>
</div>
</div>
<!-- NEW: Top Bar Button Style -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-palette"></i> Top Bar Button Style</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Pick how the main navigation buttons look. This changes their appearance only — use Tab Layout Options above to change how they are arranged.</p>
<div class="tab-style-options">
<div class="tab-style-option" data-tab-style="classic" onclick="applyTabButtonStyle('classic')">
<div class="tab-style-preview classic">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Classic</h4>
<p>Original gradient buttons</p>
</div>
<div class="tab-style-option" data-tab-style="pills" onclick="applyTabButtonStyle('pills')">
<div class="tab-style-preview pills">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Pills</h4>
<p>Fully rounded, flat fill</p>
</div>
<div class="tab-style-option" data-tab-style="underline" onclick="applyTabButtonStyle('underline')">
<div class="tab-style-preview underline">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Underline</h4>
<p>Text with an active underline</p>
</div>
<div class="tab-style-option" data-tab-style="segmented" onclick="applyTabButtonStyle('segmented')">
<div class="tab-style-preview segmented">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Segmented</h4>
<p>Joined control, one surface</p>
</div>
<div class="tab-style-option" data-tab-style="outlined" onclick="applyTabButtonStyle('outlined')">
<div class="tab-style-preview outlined">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Outlined</h4>
<p>Bordered, transparent fill</p>
</div>
<div class="tab-style-option" data-tab-style="elevated" onclick="applyTabButtonStyle('elevated')">
<div class="tab-style-preview elevated">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Elevated</h4>
<p>Raised cards with shadow</p>
</div>
<div class="tab-style-option" data-tab-style="minimal" onclick="applyTabButtonStyle('minimal')">
<div class="tab-style-preview minimal">
<span class="on">Leads</span>
<span>Jobs</span>
<span>Charts</span>
</div>
<h4>Minimal</h4>
<p>Plain text, no chrome</p>
</div>
</div>
<button onclick="saveTabButtonStyleSetting()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Button Style</span></button>
</div>
</div>
<!-- NEW: Company Name Management -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-building"></i> Company Name Management</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Manage your company names for use in lead entry</p>
<div class="form-group">
<label for="new-company-name"><i class="fas fa-plus"></i> Add New Company:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-company-name" placeholder="Enter company name">
<button onclick="addCompany()"><i class="fas fa-plus"></i> <span>Add</span></button>
</div>
</div>
<div class="form-group">
<label for="default-company-select"><i class="fas fa-star"></i> Default Company:</label>
<select id="default-company-select">
<!-- Companies will be populated by JavaScript -->
</select>
</div>
<div class="company-list-container" id="companyListContainer">
<!-- Company list will appear here -->
</div>
<button onclick="saveCompanySettings()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Company Settings</span></button>
<button onclick="resetCompanySettings()" class="danger-button" style="margin-top: 15px;"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- Theme Selection -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-palette"></i> Theme Selection</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose your preferred color theme</p>
<div class="theme-grid">
<div class="theme-option active" data-theme="default" onclick="applyTheme('default')">
<h4>Default</h4>
<div class="theme-colors">
<div class="theme-color-dot" style="background-color: #3498db;"></div>
<div class="theme-color-dot" style="background-color: #2c3e50;"></div>
<div class="theme-color-dot" style="background-color: #e74c3c;"></div>
</div>
</div>
<div class="theme-option" data-theme="dark" onclick="applyTheme('dark')">
<h4>Dark Mode</h4>
<div class="theme-colors">
<div class="theme-color-dot" style="background-color: #60a5fa;"></div>
<div class="theme-color-dot" style="background-color: #1e293b;"></div>
<div class="theme-color-dot" style="background-color: #ef4444;"></div>
</div>
</div>
<div class="theme-option" data-theme="green" onclick="applyTheme('green')">
<h4>Nature</h4>
<div class="theme-colors">
<div class="theme-color-dot" style="background-color: #4caf50;"></div>
<div class="theme-color-dot" style="background-color: #1b5e20;"></div>
<div class="theme-color-dot" style="background-color: #ff5722;"></div>
</div>
</div>
<div class="theme-option" data-theme="purple" onclick="applyTheme('purple')">
<h4>Royal</h4>
<div class="theme-colors">
<div class="theme-color-dot" style="background-color: #9c27b0;"></div>
<div class="theme-color-dot" style="background-color: #4a148c;"></div>
<div class="theme-color-dot" style="background-color: #ff6f00;"></div>
</div>
</div>
<div class="theme-option" data-theme="ocean" onclick="applyTheme('ocean')">
<h4>Ocean</h4>
<div class="theme-colors">
<div class="theme-color-dot" style="background-color: #00acc1;"></div>
<div class="theme-color-dot" style="background-color: #006064;"></div>
<div class="theme-color-dot" style="background-color: #d84315;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Button Layout -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-mouse-pointer"></i> Button Layout</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose how buttons are displayed</p>
<div class="button-layout-options">
<div class="layout-option active" data-layout="both" onclick="applyButtonLayout('both')">
<i class="fas fa-icons" style="font-size: 30px; margin-bottom: 10px;"></i>
<h4>Icon & Text</h4>
<p>Show both icons and text</p>
</div>
<div class="layout-option" data-layout="icon" onclick="applyButtonLayout('icon')">
<i class="fas fa-image" style="font-size: 30px; margin-bottom: 10px;"></i>
<h4>Icon Only</h4>
<p>Show only icons</p>
</div>
<div class="layout-option" data-layout="text" onclick="applyButtonLayout('text')">
<i class="fas fa-font" style="font-size: 30px; margin-bottom: 10px;"></i>
<h4>Text Only</h4>
<p>Show only text</p>
</div>
</div>
</div>
</div>
<!-- Button Size -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-expand-arrows-alt"></i> Button Size</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Adjust button size for better accessibility</p>
<div class="button-size-options">
<div class="size-option" data-size="small" onclick="applyButtonSize('small')">
<button style="padding: 8px 12px; font-size: 12px;"><i class="fas fa-circle"></i> Small</button>
<p>Compact buttons</p>
</div>
<div class="size-option active" data-size="medium" onclick="applyButtonSize('medium')">
<button><i class="fas fa-circle"></i> Medium</button>
<p>Default size</p>
</div>
<div class="size-option" data-size="large" onclick="applyButtonSize('large')">
<button style="padding: 14px 24px; font-size: 16px;"><i class="fas fa-circle"></i> Large</button>
<p>Larger buttons</p>
</div>
</div>
</div>
</div>
<!-- Lead Entry Settings -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-edit"></i> Lead Entry Settings</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Customize default values for lead entry form</p>
<div class="form-group">
<label for="default-time-window"><i class="fas fa-clock"></i> Default Time Window:</label>
<select id="default-time-window">
<option value="asap">ASAP (Real Time)</option>
<option value="30min">30 Minutes</option>
<option value="1hour">1 Hour</option>
<option value="2hours">2 Hours</option>
<option value="custom">Custom Time Window</option>
</select>
</div>
<div class="form-group">
<label for="default-service-option"><i class="fas fa-clipboard-list"></i> Default Service Option:</label>
<select id="default-service-option">
<option value="free-estimate">Free Estimate</option>
<option value="custom">Custom Service</option>
</select>
</div>
<button onclick="saveLeadEntrySettings()"><i class="fas fa-save"></i> <span>Save Lead Entry Settings</span></button>
<button onclick="resetLeadEntrySettings()"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- Job Details Field Control -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-tools"></i> Job Details Field Control</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose which fields to show in the Job Details form</p>
<div class="job-field-controls">
<div class="job-field-controls-grid" id="job-field-controls-grid">
<!-- Job field controls will be generated here -->
</div>
</div>
<button onclick="saveJobFieldSettings()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Job Field Settings</span></button>
<button onclick="resetJobFieldSettings()" style="margin-top: 15px;"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- Partner Settings -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-handshake"></i> Partner Settings</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Configure partner-related settings</p>
<div class="form-group">
<label for="default-partner"><i class="fas fa-user-tie"></i> Default Partner:</label>
<select id="default-partner">
<option value="">-- No Default Partner --</option>
</select>
</div>
<div class="form-group">
<label for="default-partner-commission"><i class="fas fa-percentage"></i> Default Partner Commission (%):</label>
<input type="number" id="default-partner-commission" min="0" max="100" value="50" step="1">
</div>
<button onclick="savePartnerSettings()"><i class="fas fa-save"></i> <span>Save Partner Settings</span></button>
<button onclick="resetPartnerSettings()"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- Tab Visibility & Order -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-eye"></i> Tab Visibility & Order</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Control which tabs are visible and their order</p>
<div class="tab-visibility-list" id="tab-visibility-list">
<!-- Tab visibility items will be generated here -->
</div>
<button onclick="saveTabSettings()"><i class="fas fa-save"></i> <span>Save Tab Settings</span></button>
<button onclick="resetTabSettings()"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- Lead Entry Field Visibility -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-wpforms"></i> Lead Entry Field Visibility</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Choose which fields to show in the Lead Entry form</p>
<div class="field-visibility-list" id="field-visibility-list">
<!-- Field visibility items will be generated here -->
</div>
<button onclick="saveFieldSettings()"><i class="fas fa-save"></i> <span>Save Field Settings</span></button>
<button onclick="resetFieldSettings()"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
<!-- NEW: Offer Management Settings -->
<div class="settings-section">
<div class="settings-section-header active" onclick="toggleSettingsSection(this)">
<span><i class="fas fa-gift"></i> Offer Management Settings</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="settings-section-content expanded">
<p>Manage custom offer lists and settings</p>
<div class="form-group">
<label for="new-offer-category"><i class="fas fa-plus"></i> Add New Offer Category:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-offer-category" placeholder="Category name">
<button onclick="addOfferCategory()"><i class="fas fa-plus"></i> <span>Add Category</span></button>
</div>
</div>
<div class="form-group">
<label for="new-offer-item"><i class="fas fa-plus"></i> Add New Offer Item:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="new-offer-item" placeholder="Offer item name">
<select id="offer-category-select">
<option value="">Select category</option>
</select>
<button onclick="addOfferItem()"><i class="fas fa-plus"></i> <span>Add Item</span></button>
</div>
</div>
<div id="offer-categories-list" style="margin-top: 20px;">
<!-- Offer categories and items will appear here -->
</div>
<button onclick="saveOfferSettings()" style="margin-top: 15px;"><i class="fas fa-save"></i> <span>Save Offer Settings</span></button>
<button onclick="resetOfferSettings()" class="danger-button" style="margin-top: 15px;"><i class="fas fa-undo"></i> <span>Reset to Default</span></button>
</div>
</div>
</div>
</div>
<!-- Edit Job Modal (Enhanced) -->
<div id="editJobModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditJobModal()">×</span>
<h2><i class="fas fa-edit"></i> Edit Job Details</h2>
<form id="editJobForm">
<input type="hidden" id="edit-job-id">
<div class="form-group">
<label for="edit-lead-status"><i class="fas fa-tasks"></i> Lead Status:</label>
<select id="edit-lead-status">
<option value="New">New</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
<option value="Lost">Lost</option>
</select>
</div>
<div class="form-group">
<label for="edit-technician"><i class="fas fa-user-hard-hat"></i> Technician:</label>
<select id="edit-technician">
<option value="">-- Select Technician --</option>
</select>
</div>
<div class="form-group">
<label for="edit-job-income"><i class="fas fa-dollar-sign"></i> Total Income ($):</label>
<input type="number" id="edit-job-income" step="0.01" required>
</div>
<div class="form-group">
<label for="edit-parts-cost"><i class="fas fa-box-open"></i> Parts Cost ($):</label>
<input type="number" id="edit-parts-cost" step="0.01" required>
</div>
<div class="form-group">
<label for="edit-parts-list"><i class="fas fa-list"></i> Parts Used:</label>
<textarea id="edit-parts-list" rows="3"></textarea>
</div>
<div class="form-group">
<label for="edit-job-duration"><i class="fas fa-clock"></i> Job Duration (hours):</label>
<input type="number" id="edit-job-duration" step="0.1" required>
</div>
<div class="form-group">
<label for="edit-payment-method"><i class="fas fa-credit-card"></i> Payment Method:</label>
<select id="edit-payment-method">
<option value="Cash">Cash</option>
<option value="Credit Card">Credit Card</option>
<option value="Check">Check</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="edit-job-difficulty"><i class="fas fa-tachometer-alt"></i> Job Difficulty (1-5):</label>
<select id="edit-job-difficulty">
<option value="1">1 - Very Easy</option>
<option value="2">2 - Easy</option>
<option value="3">3 - Moderate</option>
<option value="4">4 - Difficult</option>
<option value="5">5 - Very Difficult</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-satisfaction"><i class="fas fa-smile"></i> Customer Satisfaction (1-5):</label>
<select id="edit-customer-satisfaction" required>
<option value="1">1 - Very Dissatisfied</option>
<option value="2">2 - Dissatisfied</option>
<option value="3">3 - Neutral</option>
<option value="4">4 - Satisfied</option>
<option value="5">5 - Very Satisfied</option>
</select>
</div>
<div class="form-group">
<label for="edit-left-review"><i class="fas fa-comment"></i> Customer Left Review?</label>
<select id="edit-left-review" required>
<option value="No">No</option>
<option value="Yes">Yes</option>
</select>
</div>
<button type="button" onclick="saveEditedJob()"><i class="fas fa-save"></i> <span>Save Changes</span></button>
<button type="button" onclick="closeEditJobModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</form>
</div>
</div>
<!-- Edit Offer Modal -->
<div id="editOfferModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditOfferModal()">×</span>
<h2><i class="fas fa-edit"></i> Edit Offer</h2>
<form id="editOfferForm">
<input type="hidden" id="edit-offer-id">
<div class="form-group">
<label for="edit-offer-subject"><i class="fas fa-heading"></i> Subject:</label>
<input type="text" id="edit-offer-subject" required>
</div>
<div class="form-group">
<label for="edit-offer-message"><i class="fas fa-comment-dots"></i> Message:</label>
<textarea id="edit-offer-message" rows="4"></textarea>
</div>
<div class="form-group">
<label for="edit-offer-status"><i class="fas fa-tasks"></i> Status:</label>
<select id="edit-offer-status" required>
<option value="Pending">Pending</option>
<option value="Sent">Sent</option>
<option value="Accepted">Accepted</option>
<option value="Rejected">Rejected</option>
</select>
</div>
<button type="button" onclick="saveEditedOffer()"><i class="fas fa-save"></i> <span>Save Changes</span></button>
<button type="button" onclick="closeEditOfferModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</form>
</div>
</div>
<!-- Add Follow-up Modal -->
<div id="followUpModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeFollowUpModal()">×</span>
<h2><i class="fas fa-calendar-plus"></i> Add Follow-up</h2>
<form id="followUpForm">
<div class="form-group">
<label for="followup-customer"><i class="fas fa-user"></i> Select Customer:</label>
<select id="followup-customer" required>
<option value="">-- Select a customer --</option>
</select>
</div>
<div class="form-group">
<label for="followup-date"><i class="fas fa-calendar-alt"></i> Follow-up Date:</label>
<input type="date" id="followup-date" required>
</div>
<div class="form-group">
<label for="followup-time"><i class="fas fa-clock"></i> Follow-up Time:</label>
<input type="time" id="followup-time">
</div>
<div class="form-group">
<label for="followup-notes"><i class="fas fa-sticky-note"></i> Notes:</label>
<textarea id="followup-notes" rows="3" placeholder="Reason for follow-up, service needed, etc."></textarea>
</div>
<button type="button" onclick="saveFollowUp()"><i class="fas fa-save"></i> <span>Save Follow-up</span></button>
<button type="button" onclick="closeFollowUpModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</form>
</div>
</div>
<!-- Technician Detail Modal -->
<div id="techDetailModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeTechDetailModal()">×</span>
<h2><i class="fas fa-info-circle"></i> Add Technician Detail</h2>
<div class="form-group">
<label for="tech-detail-title"><i class="fas fa-heading"></i> Detail Title:</label>
<input type="text" id="tech-detail-title" placeholder="e.g., Certification, Specialty">
</div>
<div class="form-group">
<label for="tech-detail-value"><i class="fas fa-edit"></i> Detail Value:</label>
<textarea id="tech-detail-value" rows="3" placeholder="Detail information"></textarea>
</div>
<button onclick="saveTechnicianDetail()"><i class="fas fa-save"></i> <span>Save Detail</span></button>
<button onclick="closeTechDetailModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</div>
</div>
<!-- Partner Experience Modal -->
<div id="partnerExperienceModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closePartnerExperienceModal()">×</span>
<h2><i class="fas fa-star"></i> Add Work Experience</h2>
<div class="form-group">
<label for="experience-date"><i class="fas fa-calendar"></i> Date:</label>
<input type="date" id="experience-date">
</div>
<div class="form-group">
<label for="experience-rating"><i class="fas fa-star"></i> Rating (1-5):</label>
<select id="experience-rating">
<option value="5">5 - Excellent</option>
<option value="4">4 - Good</option>
<option value="3">3 - Average</option>
<option value="2">2 - Below Average</option>
<option value="1">1 - Poor</option>
</select>
</div>
<div class="form-group">
<label for="experience-difficulty"><i class="fas fa-tachometer-alt"></i> Difficulty Level (1-5):</label>
<select id="experience-difficulty">
<option value="1">1 - Very Easy</option>
<option value="2">2 - Easy</option>
<option value="3">3 - Moderate</option>
<option value="4">4 - Difficult</option>
<option value="5">5 - Very Difficult</option>
</select>
</div>
<div class="form-group">
<label for="experience-notes"><i class="fas fa-comment"></i> Notes/Review:</label>
<textarea id="experience-notes" rows="4" placeholder="Describe the experience, challenges, outcomes..."></textarea>
</div>
<button onclick="savePartnerExperience()"><i class="fas fa-save"></i> <span>Save Experience</span></button>
<button onclick="closePartnerExperienceModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</div>
</div>
<!-- Edit Strategy Modal -->
<div id="editStrategyModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditStrategyModal()">×</span>
<h2><i class="fas fa-edit"></i> Edit Strategy</h2>
<input type="hidden" id="edit-strategy-id">
<div class="form-group">
<label for="edit-strategy-name"><i class="fas fa-heading"></i> Strategy Name:</label>
<input type="text" id="edit-strategy-name" required>
</div>
<div class="form-group">
<label for="edit-strategy-description"><i class="fas fa-edit"></i> Description:</label>
<textarea id="edit-strategy-description" rows="4"></textarea>
</div>
<div class="form-group">
<label for="edit-strategy-success-rate"><i class="fas fa-percentage"></i> Success Rate (%):</label>
<input type="number" id="edit-strategy-success-rate" min="0" max="100">
</div>
<button onclick="saveEditedStrategy()"><i class="fas fa-save"></i> <span>Save Changes</span></button>
<button onclick="closeEditStrategyModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</div>
</div>
<!-- Edit Mistake Modal -->
<div id="editMistakeModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditMistakeModal()">×</span>
<h2><i class="fas fa-edit"></i> Edit Mistake</h2>
<input type="hidden" id="edit-mistake-id">
<div class="form-group">
<label for="edit-mistake-name"><i class="fas fa-heading"></i> Mistake Description:</label>
<input type="text" id="edit-mistake-name" required>
</div>
<div class="form-group">
<label for="edit-mistake-solution"><i class="fas fa-lightbulb"></i> Solution:</label>
<textarea id="edit-mistake-solution" rows="4"></textarea>
</div>
<div class="form-group">
<label for="edit-mistake-frequency"><i class="fas fa-chart-bar"></i> Frequency (1-5):</label>
<select id="edit-mistake-frequency">
<option value="1">1 - Rare</option>
<option value="2">2 - Occasional</option>
<option value="3">3 - Moderate</option>
<option value="4">4 - Frequent</option>
<option value="5">5 - Very Common</option>
</select>
</div>
<button onclick="saveEditedMistake()"><i class="fas fa-save"></i> <span>Save Changes</span></button>
<button onclick="closeEditMistakeModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</div>
</div>
<!-- Edit Customer Modal -->
<div id="editCustomerModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditCustomerModal()">×</span>
<h2><i class="fas fa-edit"></i> Edit Customer Details</h2>
<form id="editCustomerForm">
<input type="hidden" id="edit-customer-id">
<div class="form-group">
<label for="edit-customer-name"><i class="fas fa-user"></i> Customer Name:</label>
<input type="text" id="edit-customer-name" required>
</div>
<div class="form-group">
<label for="edit-customer-address"><i class="fas fa-map-marker-alt"></i> Address:</label>
<input type="text" id="edit-customer-address" required>
</div>
<div class="form-group">
<label for="edit-customer-phone"><i class="fas fa-phone"></i> Phone Number:</label>
<input type="tel" id="edit-customer-phone" required>
</div>
<div class="form-group">
<label for="edit-customer-email"><i class="fas fa-envelope"></i> Email:</label>
<input type="email" id="edit-customer-email">
</div>
<div class="form-group">
<label for="edit-customer-house-value"><i class="fas fa-home"></i> Estimated House Value:</label>
<select id="edit-customer-house-value">
<option value="">-- Select Range --</option>
<option value="Under $300k">Under $300k</option>
<option value="$300k - $500k">$300k - $500k</option>
<option value="$500k - $750k">$500k - $750k</option>
<option value="$750k - $1M">$750k - $1M</option>
<option value="Over $1M">Over $1M</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-garage-type"><i class="fas fa-door-closed"></i> Garage Type:</label>
<select id="edit-customer-garage-type">
<option value="">-- Select Type --</option>
<option value="Attached">Attached</option>
<option value="Detached">Detached</option>
<option value="Carport">Carport</option>
<option value="Unknown">Unknown</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-garage-size"><i class="fas fa-ruler-combined"></i> Garage Size:</label>
<select id="edit-customer-garage-size">
<option value="">-- Select Size --</option>
<option value="1-car">1-car</option>
<option value="2-car">2-car</option>
<option value="3-car">3-car</option>
<option value="4+ car">4+ car</option>
<option value="Unknown">Unknown</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-door-age"><i class="fas fa-calendar"></i> Door Age (approx.):</label>
<select id="edit-customer-door-age">
<option value="">-- Select Age --</option>
<option value="Less than 5 years">Less than 5 years</option>
<option value="5-10 years">5-10 years</option>
<option value="10-15 years">10-15 years</option>
<option value="15-20 years">15-20 years</option>
<option value="Over 20 years">Over 20 years</option>
<option value="Unknown">Unknown</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-door-type"><i class="fas fa-door-open"></i> Door Type:</label>
<select id="edit-customer-door-type">
<option value="">-- Select Type --</option>
<option value="Sectional">Sectional</option>
<option value="Roll-up">Roll-up</option>
<option value="Side-hinged">Side-hinged</option>
<option value="Tilt-up/Canopy">Tilt-up/Canopy</option>
<option value="Unknown">Unknown</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-opener-type"><i class="fas fa-cogs"></i> Opener Type:</label>
<select id="edit-customer-opener-type">
<option value="">-- Select Type --</option>
<option value="Chain Drive">Chain Drive</option>
<option value="Belt Drive">Belt Drive</option>
<option value="Screw Drive">Screw Drive</option>
<option value="Jackshaft">Jackshaft</option>
<option value="None/Manual">None/Manual</option>
<option value="Unknown">Unknown</option>
</select>
</div>
<div class="form-group">
<label for="edit-customer-notes"><i class="fas fa-sticky-note"></i> Additional Notes:</label>
<textarea id="edit-customer-notes" rows="4" placeholder="Any additional customer information, preferences, or observations"></textarea>
</div>
<div class="form-group">
<label for="edit-customer-future-services"><i class="fas fa-lightbulb"></i> Recommended Future Services:</label>
<textarea id="edit-customer-future-services" rows="3" placeholder="Suggest future services based on customer's situation"></textarea>
</div>
<button type="button" onclick="saveEditedCustomer()"><i class="fas fa-save"></i> <span>Save Changes</span></button>
<button type="button" onclick="closeEditCustomerModal()" class="danger-button"><i class="fas fa-times"></i> <span>Cancel</span></button>
</form>
</div>
</div>
<!-- Job Details Modal for Calendar -->
<div id="jobDetailsModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeJobDetailsModal()">×</span>
<h2><i class="fas fa-info-circle"></i> Job Details</h2>
<div id="jobDetailsContent">
<!-- Job details will appear here -->
</div>
<button onclick="closeJobDetailsModal()" class="danger-button" style="margin-top: 15px;"><i class="fas fa-times"></i> <span>Close</span></button>
</div>
</div>
<script>
// --- FIX: escape values injected into inline onclick="..." attributes ---
function escAttr(v) {
return String(v == null ? '' : v)
.replace(/&/g, '&')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
// Store data in memory (localStorage used for persistence)
let leads = JSON.parse(localStorage.getItem('garageDoorLeads')) || [];
let jobs = JSON.parse(localStorage.getItem('garageDoorJobs')) || [];
let offers = JSON.parse(localStorage.getItem('garageDoorOffers')) || [];
let customers = JSON.parse(localStorage.getItem('garageDoorCustomers')) || [];
let technicians = JSON.parse(localStorage.getItem('garageDoorTechnicians')) || [];
let strategies = JSON.parse(localStorage.getItem('garageDoorStrategies')) || [];
let mistakes = JSON.parse(localStorage.getItem('garageDoorMistakes')) || [];
let partners = JSON.parse(localStorage.getItem('garageDoorPartners')) || [];
let payments = JSON.parse(localStorage.getItem('garageDoorPayments')) || [];
let followups = JSON.parse(localStorage.getItem('garageDoorFollowups')) || [];
let reminders = JSON.parse(localStorage.getItem('garageDoorReminders')) || [];
let techDetails = JSON.parse(localStorage.getItem('garageDoorTechDetails')) || {};
let partnerExperiences = JSON.parse(localStorage.getItem('garageDoorPartnerExperiences')) || {};
let customServices = JSON.parse(localStorage.getItem('garageDoorCustomServices')) || [];
let companies = JSON.parse(localStorage.getItem('garageDoorCompanies')) || [
{ id: 1, name: 'Murrieta Garage Doors', isDefault: true }
];
let offerCategories = JSON.parse(localStorage.getItem('garageDoorOfferCategories')) || [
{
id: 1,
name: 'Maintenance Services',
items: [
'Spring Replacement Package',
'Annual Lubrication Service',
'Full System Tune-Up'
]
},
{
id: 2,
name: 'Upgrade Services',
items: [
'Epoxy Floor Coating',
'New Garage Door Installation',
'Smart Opener Upgrade'
]
},
{
id: 3,
name: 'Custom Services',
items: [
'Garage Gym Installation',
'Custom Storage Solutions',
'Custom Design Package'
]
}
];
const DEFAULT_SETTINGS = {
theme: 'default',
buttonLayout: 'both',
buttonSize: 'medium',
tabLayout: 'horizontal',
titleDisplay: 'show',
tabButtonStyle: 'classic',
tabVisibility: {},
fieldVisibility: {},
jobFieldVisibility: {},
leadEntry: {
defaultTimeWindow: 'asap',
defaultServiceOption: 'free-estimate'
},
partnerSettings: {
defaultPartner: '',
defaultPartnerCommission: 50
},
reminders: {
enabled: false,
callReminder: false,
emailReminder: false,
maintenanceReminder: false,
reviewReminder: false
}
};
// FIX: deep-merge stored settings over defaults; a partial object (e.g. from an
// imported backup with no settings key) used to make applyTheme() crash on load.
function mergeSettings(saved) {
const base = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
if (!saved || typeof saved !== 'object') return base;
Object.keys(saved).forEach(k => {
const v = saved[k];
if (v && typeof v === 'object' && !Array.isArray(v) && base[k] && typeof base[k] === 'object') {
base[k] = Object.assign({}, base[k], v);
} else if (v !== undefined && v !== null) {
base[k] = v;
}
});
return base;
}
let settings = mergeSettings(JSON.parse(localStorage.getItem('garageDoorSettings')));
// Chart instances
let incomeChart, sourceChart, profitChart, satisfactionChart, offerChart, projectionChart;
// Current sort state for tables
let currentSortColumn = null;
let currentSortDirection = 1;
// Calendar state
let currentCalendarDate = new Date();
// Current technician for details
let currentTechnicianName = null;
// Current partner for details
let currentPartnerName = null;
// Drag and drop state
let draggedLead = null;
// My Account state
let myAccountFilterPaid = false;
// Tab navigation
function openTab(tabName) {
const tabs = document.getElementsByClassName('tab');
for (let i = 0; i < tabs.length; i++) {
tabs[i].classList.remove('active');
}
const tabContents = document.getElementsByClassName('tab-content');
for (let i = 0; i < tabContents.length; i++) {
tabContents[i].classList.remove('active');
}
const targetTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
if (targetTab) {
targetTab.classList.add('active');
}
const targetContent = document.getElementById(tabName);
if (targetContent) {
targetContent.classList.add('active');
}
if (tabName === 'job-details') {
updateLeadDropdown();
updateTechnicianDropdown();
updatePartnerDropdown();
updateCompletedJobsList();
} else if (tabName === 'performance') {
updatePerformanceStats();
} else if (tabName === 'charts') {
updateCharts();
} else if (tabName === 'customer-management') {
updateCustomerList();
} else if (tabName === 'offers') {
updateCustomerDropdown();
updateAllOffersList();
updateOfferCategoriesDropdown();
updateOfferCategoriesList();
generateOfferDropdown();
} else if (tabName === 'projections') {
initializeProjectionDates();
} else if (tabName === 'technicians') {
updateTechniciansList();
} else if (tabName === 'my-account') {
updateMyAccount();
} else if (tabName === 'strategies') {
updateStrategiesList();
} else if (tabName === 'mistakes') {
updateMistakesList();
} else if (tabName === 'partner-management') {
updatePartnersList();
} else if (tabName === 'calendar') {
renderCalendar();
updateUpcomingReminders();
} else if (tabName === 'settings') {
initializeSettings();
}
}
// Toggle compact sections
function toggleCompactSection(header) {
header.classList.toggle('collapsed');
header.classList.toggle('active');
const content = header.nextElementSibling;
content.classList.toggle('expanded');
}
// NEW: Toggle settings sections
function toggleSettingsSection(header) {
header.classList.toggle('collapsed');
header.classList.toggle('active');
const content = header.nextElementSibling;
content.classList.toggle('expanded');
}
// Toggle sheet sections
function toggleSheetSection(header) {
header.classList.toggle('collapsed');
const content = header.nextElementSibling;
content.style.display = content.style.display === 'none' ? 'block' : 'none';
}
// Toggle payment type (percentage vs flat rate)
function togglePaymentType() {
const paymentType = document.querySelector('input[name="payment-type"]:checked').value;
const percentageSection = document.getElementById('percentage-section');
const flatRateSection = document.getElementById('flat-rate-section');
if (paymentType === 'percentage') {
percentageSection.style.display = 'block';
flatRateSection.style.display = 'none';
} else {
percentageSection.style.display = 'none';
flatRateSection.style.display = 'block';
}
}
// Update commission value display
function updateCommissionValue() {
const slider = document.getElementById('technician-commission');
const display = document.getElementById('commission-value');
display.textContent = `${slider.value}%`;
// Update slider background
const percentage = (slider.value - slider.min) / (slider.max - slider.min) * 100;
slider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${percentage}%, #ddd ${percentage}%, #ddd 100%)`;
}
// Update partner commission value display
function updatePartnerCommissionValue() {
const slider = document.getElementById('partner-commission');
const display = document.getElementById('partner-commission-value');
display.textContent = `${slider.value}%`;
// Update slider background
const percentage = (slider.value - slider.min) / (slider.max - slider.min) * 100;
slider.style.background = `linear-gradient(to right, var(--primary-color) 0%, var(--primary-color) ${percentage}%, #ddd ${percentage}%, #ddd 100%)`;
}
// Update partner commission from select
function updatePartnerCommissionFromSelect() {
const partnerSelect = document.getElementById('partner-name');
const selectedPartner = partners.find(p => p.name === partnerSelect.value);
if (selectedPartner) {
document.getElementById('partner-commission').value = selectedPartner.commissionRate;
updatePartnerCommissionValue();
document.getElementById('paymentManagement').style.display = 'block';
updatePartnerPaymentInfo(selectedPartner);
} else {
document.getElementById('partner-commission').value = 50;
updatePartnerCommissionValue();
document.getElementById('paymentManagement').style.display = 'none';
}
}
// Filter leads by status
function filterLeads(filter) {
const filterButtons = document.querySelectorAll('.lead-filter button');
filterButtons.forEach(btn => btn.classList.remove('active'));
// FIX: event.target is the <i>/<span> inside the button when the icon is clicked
const _btn = window.event && (window.event.currentTarget || window.event.target.closest('.lead-filter button'));
if (_btn && _btn.classList) _btn.classList.add('active');
updateLeadDropdown(filter);
}
// Generate lead information
function generateLead() {
const name = document.getElementById('customer-name').value;
const address = document.getElementById('customer-address').value;
const phone = document.getElementById('customer-phone').value;
const email = document.getElementById('customer-email').value;
const company = document.getElementById('company').value;
const date = document.getElementById('service-date').value;
const startTime = document.getElementById('start-time').value;
const endTime = document.getElementById('end-time').value;
const notes = document.getElementById('service-notes').value;
const pricing = document.getElementById('service-pricing').value;
const source = document.getElementById('marketing-source').value;
if (!name || !address || !phone || !date || !startTime || !endTime) {
alert('Please fill in all required fields');
return;
}
// Check if free estimate is selected
const isFreeEstimate = document.querySelector('.service-option.active').textContent.includes('Free Estimate');
if (!isFreeEstimate && !notes) {
alert('Please describe the service needed');
return;
}
// Format time window
const formatTime = (timeStr) => {
const [hours, minutes] = timeStr.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${ampm}`;
};
const timeWindow = `${formatTime(startTime)} - ${formatTime(endTime)}`;
const formattedDate = new Date(date).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit'});
const finalNotes = isFreeEstimate ? 'Free Estimate' : notes;
const leadText = `Name: ${name}\nAddress: ${address}\nNumber: ${phone}\n${email ? 'Email: ' + email + '\n' : ''}Company: ${company}\nDate: ${formattedDate}\nTime: ${timeWindow}\nSource: ${source}\nNotes: ${finalNotes}${pricing ? '\n' + pricing : ''}`;
document.getElementById('leadOutput').textContent = leadText;
document.getElementById('leadOutput').style.display = 'block';
document.getElementById('shareButtons').style.display = 'block';
}
// Set time window from preset buttons
function setTimeWindow(start, end) {
document.getElementById('start-time').value = start;
document.getElementById('end-time').value = end;
// Update active state of buttons
const buttons = document.querySelectorAll('.time-preset-button');
buttons.forEach(btn => btn.classList.remove('active'));
const _btn = window.event && (window.event.currentTarget || window.event.target.closest('.time-preset-button'));
if (_btn && _btn.classList) _btn.classList.add('active');
}
// Share functions
function shareViaWhatsApp() {
const leadText = document.getElementById('leadOutput').textContent;
const encodedText = encodeURIComponent(leadText);
window.open(`https://wa.me/?text=${encodedText}`);
}
function copyToClipboard() {
const leadText = document.getElementById('leadOutput').textContent;
navigator.clipboard.writeText(leadText).then(() => {
alert('Lead information copied to clipboard!');
});
}
function shareViaEmail() {
const leadText = document.getElementById('leadOutput').textContent;
const subject = 'New Garage Door Service Lead';
const body = encodeURIComponent(leadText);
window.open(`mailto:?subject=${subject}&body=${body}`);
}
function addToGoogleCalendar() {
const name = document.getElementById('customer-name').value;
const address = document.getElementById('customer-address').value;
const phone = document.getElementById('customer-phone').value;
const date = document.getElementById('service-date').value;
const startTime = document.getElementById('start-time').value;
const endTime = document.getElementById('end-time').value;
const notes = document.getElementById('service-notes').value;
if (!date || !startTime || !endTime || !name) {
alert('Please fill in date, time, and customer name first');
return;
}
// Parse start time
const [startHours, startMinutes] = startTime.split(':');
const startHour = parseInt(startHours);
// Parse end time
const [endHours, endMinutes] = endTime.split(':');
const endHour = parseInt(endHours);
// Create date-time strings
const startDateTime = `${date}T${startHours.padStart(2, '0')}:${startMinutes}:00`;
const endDateTime = `${date}T${endHours.padStart(2, '0')}:${endMinutes}:00`;
const startDate = new Date(startDateTime);
const endDate = new Date(endDateTime);
// Format for Google Calendar
const formatDate = (d) => {
return d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
};
const isFreeEstimate = document.querySelector('.service-option.active').textContent.includes('Free Estimate');
const finalNotes = isFreeEstimate ? 'Free Estimate' : notes;
const title = encodeURIComponent(`Garage Door Service - ${name}`);
const description = encodeURIComponent(`Customer: ${name}\nPhone: ${phone}\nAddress: ${address}\nService: ${finalNotes}`);
const location = encodeURIComponent(address);
const dates = `${formatDate(startDate)}/${formatDate(endDate)}`;
const googleCalendarUrl = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${title}&dates=${dates}&details=${description}&location=${location}`;
window.open(googleCalendarUrl, '_blank');
}
// Save lead
function saveLead() {
const name = document.getElementById('customer-name').value;
const address = document.getElementById('customer-address').value;
const phone = document.getElementById('customer-phone').value;
const email = document.getElementById('customer-email').value;
const company = document.getElementById('company').value;
const date = document.getElementById('service-date').value;
const startTime = document.getElementById('start-time').value;
const endTime = document.getElementById('end-time').value;
const notes = document.getElementById('service-notes').value;
const pricing = document.getElementById('service-pricing').value;
const source = document.getElementById('marketing-source').value;
if (!name || !address || !phone || !date || !startTime || !endTime) {
alert('Please fill in all required fields');
return;
}
// Check if free estimate is selected
const isFreeEstimate = document.querySelector('.service-option.active').textContent.includes('Free Estimate');
if (!isFreeEstimate && !notes) {
alert('Please describe the service needed');
return;
}
// Format time window
const formatTime = (timeStr) => {
const [hours, minutes] = timeStr.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${ampm}`;
};
const timeWindow = `${formatTime(startTime)} - ${formatTime(endTime)}`;
const finalNotes = isFreeEstimate ? 'Free Estimate' : notes;
const lead = {
id: Date.now(),
name,
address,
phone,
email,
company,
date,
time: timeWindow,
startTime,
endTime,
notes: finalNotes,
pricing,
source,
status: 'New',
createdAt: new Date().toISOString()
};
leads.push(lead);
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
if (!customers.some(c => c.phone === phone)) {
const customer = {
id: lead.id,
name,
address,
phone,
email,
firstServiceDate: date,
lastServiceDate: date,
services: [finalNotes],
status: 'Active'
};
customers.push(customer);
localStorage.setItem('garageDoorCustomers', JSON.stringify(customers));
}
alert('Lead saved successfully!');
document.getElementById('leadForm').reset();
document.getElementById('leadOutput').style.display = 'none';
document.getElementById('shareButtons').style.display = 'none';
updateLeadGeneratedCount();
renderCalendar();
// Reset time to default
document.getElementById('start-time').value = '16:00';
document.getElementById('end-time').value = '17:00';
// Reset service option to default
selectServiceOption('free-estimate');
}
// Clear form function
function clearForm() {
document.getElementById('leadForm').reset();
document.getElementById('leadOutput').style.display = 'none';
document.getElementById('shareButtons').style.display = 'none';
selectServiceOption('free-estimate');
}
// Update lead dropdown
function updateLeadDropdown(filter = 'all') {
const select = document.getElementById('select-lead');
select.innerHTML = '<option value="">-- Select a saved lead --</option>';
leads.forEach(lead => {
let showLead = true;
if (filter === 'new' && lead.status !== 'New') showLead = false;
if (filter === 'progress' && lead.status !== 'In Progress') showLead = false;
if (filter === 'completed' && lead.status !== 'Completed') showLead = false;
if (showLead) {
const option = document.createElement('option');
option.value = lead.id;
option.textContent = `${lead.name} - ${lead.notes.substring(0, 30)}... (${new Date(lead.date).toLocaleDateString()}) - ${lead.status}`;
select.appendChild(option);
}
});
}
// Update technician dropdown
function updateTechnicianDropdown() {
const select = document.getElementById('technician-name');
const editSelect = document.getElementById('edit-technician');
select.innerHTML = '<option value="">-- Select Technician --</option>';
if (editSelect) {
editSelect.innerHTML = '<option value="">-- Select Technician --</option>';
}
technicians.forEach(tech => {
const option = document.createElement('option');
option.value = tech.name;
option.textContent = tech.name;
select.appendChild(option);
if (editSelect) {
const editOption = option.cloneNode(true);
editSelect.appendChild(editOption);
}
});
}
// Update partner dropdown
function updatePartnerDropdown() {
const select = document.getElementById('partner-name');
select.innerHTML = '<option value="">-- No Partner --</option>';
partners.forEach(partner => {
const option = document.createElement('option');
option.value = partner.name;
option.textContent = `${partner.name} (${partner.commissionRate}% commission)`;
select.appendChild(option);
});
// Update settings dropdown as well
const settingsSelect = document.getElementById('default-partner');
if (settingsSelect) {
settingsSelect.innerHTML = '<option value="">-- No Default Partner --</option>';
partners.forEach(partner => {
const option = document.createElement('option');
option.value = partner.name;
option.textContent = partner.name;
settingsSelect.appendChild(option);
});
// Set the current default partner if one exists
if (settings.partnerSettings && settings.partnerSettings.defaultPartner) {
settingsSelect.value = settings.partnerSettings.defaultPartner;
}
}
}
// Load lead details
function loadLeadDetails() {
const leadId = parseInt(document.getElementById('select-lead').value);
if (!leadId) return;
const lead = leads.find(l => l.id === leadId);
if (lead) {
document.getElementById('lead-status-job').value = lead.status || 'New';
}
}
// NEW: Create reminders for a completed job
function createJobReminders(job, lead) {
if (!settings.reminders || !settings.reminders.enabled) return;
const jobDate = new Date(job.completedAt);
const customerName = lead.name;
const customerPhone = lead.phone;
// 1. Call reminder - day after completion
if (settings.reminders.callReminder) {
const callDate = new Date(jobDate);
callDate.setDate(callDate.getDate() + 1);
reminders.push({
id: Date.now() + Math.random(),
type: 'call',
customerName: customerName,
customerPhone: customerPhone,
date: callDate.toISOString(),
jobId: job.id,
notes: 'Follow-up call to ensure customer satisfaction',
completed: false
});
}
// 2. Email reminder - 30 days after service
if (settings.reminders.emailReminder) {
const emailDate = new Date(jobDate);
emailDate.setDate(emailDate.getDate() + 30);
reminders.push({
id: Date.now() + Math.random() + 1,
type: 'email',
customerName: customerName,
customerPhone: customerPhone,
date: emailDate.toISOString(),
jobId: job.id,
notes: 'Send follow-up email with maintenance tips',
completed: false
});
}
// 3. Annual maintenance reminder - 365 days
if (settings.reminders.maintenanceReminder) {
const maintenanceDate = new Date(jobDate);
maintenanceDate.setDate(maintenanceDate.getDate() + 365);
reminders.push({
id: Date.now() + Math.random() + 2,
type: 'maintenance',
customerName: customerName,
customerPhone: customerPhone,
date: maintenanceDate.toISOString(),
jobId: job.id,
notes: 'Annual maintenance reminder',
completed: false
});
}
// 4. Review request - same day (if satisfaction >= 4)
if (settings.reminders.reviewReminder && job.satisfaction >= 4) {
const reviewDate = new Date(jobDate);
// Set to end of the same day
reviewDate.setHours(20, 0, 0, 0);
reminders.push({
id: Date.now() + Math.random() + 3,
type: 'review',
customerName: customerName,
customerPhone: customerPhone,
date: reviewDate.toISOString(),
jobId: job.id,
notes: 'Request review from satisfied customer',
completed: false
});
}
localStorage.setItem('garageDoorReminders', JSON.stringify(reminders));
}
// Save job details
function saveJobDetails() {
const leadId = parseInt(document.getElementById('select-lead').value);
if (!leadId) {
alert('Please select a lead first');
return;
}
const lead = leads.find(l => l.id === leadId);
if (!lead) return;
const leadStatus = document.getElementById('lead-status-job').value;
const technician = document.getElementById('technician-name').value;
const partner = document.getElementById('partner-name').value;
const paymentType = document.querySelector('input[name="payment-type"]:checked').value;
const commissionRate = paymentType === 'percentage' ? parseInt(document.getElementById('technician-commission').value) || 0 : 0;
const flatRateAmount = paymentType === 'flat' ? parseFloat(document.getElementById('flat-rate-amount').value) || 0 : 0;
const partnerCommissionRate = partner ? parseInt(document.getElementById('partner-commission').value) || 0 : 0;
const income = parseFloat(document.getElementById('job-income').value) || 0;
const paymentMethod = document.getElementById('payment-method').value;
const partsCost = parseFloat(document.getElementById('parts-cost').value) || 0;
const partsList = document.getElementById('parts-list').value;
const duration = parseFloat(document.getElementById('job-duration').value) || 0;
const strategy = document.getElementById('sale-strategy').value;
const difficulty = document.getElementById('job-difficulty').value;
const satisfaction = document.getElementById('customer-satisfaction').value;
const review = document.getElementById('left-review').value;
const job = {
id: Date.now(),
leadId,
technician,
partner,
paymentType,
commissionRate,
flatRateAmount,
partnerCommissionRate,
income,
paymentMethod,
partsCost,
partsList,
duration,
strategy,
difficulty,
satisfaction,
review,
completedAt: new Date().toISOString(),
paid: false // Track payment status for My Account
};
jobs.push(job);
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
const leadIndex = leads.findIndex(l => l.id === leadId);
if (leadIndex !== -1) {
leads[leadIndex].status = leadStatus;
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
}
const customerIndex = customers.findIndex(c => c.phone === lead.phone);
if (customerIndex !== -1) {
customers[customerIndex].lastServiceDate = lead.date;
if (!customers[customerIndex].services.includes(lead.notes)) {
customers[customerIndex].services.push(lead.notes);
}
localStorage.setItem('garageDoorCustomers', JSON.stringify(customers));
}
if (technician) {
const techIndex = technicians.findIndex(t => t.name === technician);
const techCommission = paymentType === 'flat' ? flatRateAmount : (income - partsCost) * (commissionRate / 100);
if (techIndex === -1) {
technicians.push({
name: technician,
jobs: 1,
totalIncome: income,
totalHours: duration,
totalCommission: techCommission,
avgSatisfaction: parseFloat(satisfaction),
lastJob: new Date().toISOString()
});
} else {
technicians[techIndex].jobs += 1;
technicians[techIndex].totalIncome += income;
technicians[techIndex].totalHours += duration;
technicians[techIndex].totalCommission += techCommission;
const totalJobs = technicians[techIndex].jobs;
const prevTotal = (technicians[techIndex].avgSatisfaction || 3) * (totalJobs - 1);
technicians[techIndex].avgSatisfaction = (prevTotal + parseFloat(satisfaction)) / totalJobs;
technicians[techIndex].lastJob = new Date().toISOString();
}
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
}
// NEW: Create reminders for this job
if (leadStatus === 'Completed') {
createJobReminders(job, lead);
}
alert('Job details saved successfully!');
document.getElementById('jobForm').reset();
updateLeadDropdown();
updatePerformanceStats();
updateCustomerList();
updateTechniciansList();
updateCompletedJobsList();
renderCalendar();
updateUpcomingReminders();
// Update My Account if it's open
if (document.getElementById('my-account').classList.contains('active')) {
updateMyAccount();
}
}
// Calculate profit
function calculateProfit() {
const income = parseFloat(document.getElementById('job-income').value) || 0;
const partsCost = parseFloat(document.getElementById('parts-cost').value) || 0;
const paymentType = document.querySelector('input[name="payment-type"]:checked').value;
const commissionRate = paymentType === 'percentage' ? parseInt(document.getElementById('technician-commission').value) || 0 : 0;
const flatRateAmount = paymentType === 'flat' ? parseFloat(document.getElementById('flat-rate-amount').value) || 0 : 0;
const partnerCommissionRate = document.getElementById('partner-name').value ? parseInt(document.getElementById('partner-commission').value) || 50 : 0;
const partner = document.getElementById('partner-name').value;
const duration = parseFloat(document.getElementById('job-duration').value) || 0;
const grossProfit = income - partsCost;
const technicianCommission = paymentType === 'flat' ? flatRateAmount : grossProfit * (commissionRate / 100);
const netAfterTechnician = grossProfit - technicianCommission;
const partnerCommission = partner ? netAfterTechnician * (partnerCommissionRate / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
const hourlyRate = duration > 0 ? netProfit / duration : 0;
const techHourlyRate = duration > 0 ? technicianCommission / duration : 0;
let profitText = `Job Profit Breakdown:\n\nTotal Income: $${income.toFixed(2)}\nParts Cost: $${partsCost.toFixed(2)}\nGross Profit: $${grossProfit.toFixed(2)}\n`;
if (paymentType === 'flat') {
profitText += `Technician Flat Rate: $${flatRateAmount.toFixed(2)}\n`;
} else {
profitText += `Technician Commission (${commissionRate}%): $${technicianCommission.toFixed(2)}\n`;
}
profitText += `Technician Hourly Rate: $${techHourlyRate.toFixed(2)}/hr`;
if (partner) {
profitText += `\nPartner Commission (${partnerCommissionRate}% of remaining to ${partner}): $${partnerCommission.toFixed(2)}`;
}
profitText += `\n\nYour Net Profit: $${netProfit.toFixed(2)}\nYour Hourly Rate: $${hourlyRate.toFixed(2)}/hr`;
document.getElementById('profitOutput').textContent = profitText;
document.getElementById('profitCalculation').style.display = 'block';
}
// Share profit functions
function shareProfitViaWhatsApp() {
const profitText = document.getElementById('profitOutput').textContent;
window.open(`https://wa.me/?text=${encodeURIComponent(profitText)}`);
}
function copyProfitToClipboard() {
const profitText = document.getElementById('profitOutput').textContent;
navigator.clipboard.writeText(profitText).then(() => {
alert('Profit breakdown copied to clipboard!');
});
}
function shareProfitViaEmail() {
const profitText = document.getElementById('profitOutput').textContent;
window.open(`mailto:?subject=Job Profit Breakdown&body=${encodeURIComponent(profitText)}`);
}
// Update completed jobs list
function updateCompletedJobsList() {
const listDiv = document.getElementById('completedJobsList');
listDiv.innerHTML = '';
if (jobs.length === 0) {
listDiv.innerHTML = '<p>No completed jobs yet.</p>';
return;
}
const sortedJobs = jobs.slice().sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
sortedJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const jobDate = new Date(job.completedAt).toLocaleDateString();
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
const jobCard = document.createElement('div');
jobCard.className = 'job-item';
jobCard.innerHTML = `
<button class="job-delete-btn danger-button" onclick="deleteJob(${job.id})"><i class="fas fa-trash"></i></button>
<button class="job-edit-btn" onclick="openEditJobModal(${job.id})"><i class="fas fa-edit"></i> <span>Edit</span></button>
<div class="job-header">
<span>${jobDate}</span>
<span>$${(job.income || 0).toFixed(2)}</span>
</div>
<p><strong>Customer:</strong> ${lead.name}</p>
<p><strong>Service:</strong> ${lead.notes}</p>
<p><strong>Status:</strong> ${lead.status}</p>
<p><strong>Technician:</strong> ${job.technician || 'N/A'}</p>
<p><strong>Payment Type:</strong> ${job.paymentType === 'flat' ? 'Flat Rate' : 'Percentage'}</p>
<p><strong>Tech Payment:</strong> $${techCommission.toFixed(2)}</p>
<p><strong>Duration:</strong> ${job.duration || 0} hours</p>
<p><strong>Parts Cost:</strong> $${(job.partsCost || 0).toFixed(2)}</p>
<p><strong>Net Profit:</strong> $${netProfit.toFixed(2)}</p>
<p><strong>Satisfaction:</strong> ${job.satisfaction || '-'}/5</p>
<p><strong>Review:</strong> ${job.review || 'No'}</p>
`;
listDiv.appendChild(jobCard);
});
}
// Delete job function
function deleteJob(jobId) {
if (confirm('Are you sure you want to delete this job? This action cannot be undone.')) {
const jobIndex = jobs.findIndex(j => j.id === jobId);
if (jobIndex !== -1) {
jobs.splice(jobIndex, 1);
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
updateCompletedJobsList();
updatePerformanceStats();
updateCharts();
renderCalendar();
alert('Job deleted successfully!');
}
}
}
// Edit job modal functions
function openEditJobModal(jobId) {
const job = jobs.find(j => j.id === jobId);
if (!job) return;
const lead = leads.find(l => l.id === job.leadId);
document.getElementById('edit-job-id').value = jobId;
document.getElementById('edit-lead-status').value = lead ? lead.status : 'Completed';
document.getElementById('edit-technician').value = job.technician || '';
document.getElementById('edit-job-income').value = job.income || 0;
document.getElementById('edit-parts-cost').value = job.partsCost || 0;
document.getElementById('edit-parts-list').value = job.partsList || '';
document.getElementById('edit-job-duration').value = job.duration || 0;
document.getElementById('edit-payment-method').value = job.paymentMethod || 'Cash';
document.getElementById('edit-job-difficulty').value = job.difficulty || 3;
document.getElementById('edit-customer-satisfaction').value = job.satisfaction || 3;
document.getElementById('edit-left-review').value = job.review || 'No';
updateTechnicianDropdown();
document.getElementById('editJobModal').style.display = 'block';
}
function closeEditJobModal() {
document.getElementById('editJobModal').style.display = 'none';
}
function saveEditedJob() {
const jobId = parseInt(document.getElementById('edit-job-id').value);
const jobIndex = jobs.findIndex(j => j.id === jobId);
if (jobIndex === -1) {
alert('Job not found');
return;
}
const lead = leads.find(l => l.id === jobs[jobIndex].leadId);
if (lead) {
lead.status = document.getElementById('edit-lead-status').value;
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
}
jobs[jobIndex].technician = document.getElementById('edit-technician').value;
jobs[jobIndex].income = parseFloat(document.getElementById('edit-job-income').value) || 0;
jobs[jobIndex].partsCost = parseFloat(document.getElementById('edit-parts-cost').value) || 0;
jobs[jobIndex].partsList = document.getElementById('edit-parts-list').value;
jobs[jobIndex].duration = parseFloat(document.getElementById('edit-job-duration').value) || 0;
jobs[jobIndex].paymentMethod = document.getElementById('edit-payment-method').value;
jobs[jobIndex].difficulty = document.getElementById('edit-job-difficulty').value;
jobs[jobIndex].satisfaction = document.getElementById('edit-customer-satisfaction').value;
jobs[jobIndex].satisfaction = document.getElementById('edit-customer-satisfaction').value;
jobs[jobIndex].review = document.getElementById('edit-left-review').value;
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
// Update technician stats if technician changed
if (jobs[jobIndex].technician) {
updateTechnicianStats(jobs[jobIndex].technician);
}
closeEditJobModal();
updateCompletedJobsList();
updatePerformanceStats();
updateCharts();
renderCalendar();
alert('Job updated successfully!');
}
// Update technician stats
function updateTechnicianStats(techName) {
const techJobs = jobs.filter(j => j.technician === techName);
const techIndex = technicians.findIndex(t => t.name === techName);
if (techIndex === -1) {
technicians.push({
name: techName,
jobs: techJobs.length,
totalIncome: techJobs.reduce((sum, job) => sum + (job.income || 0), 0),
totalHours: techJobs.reduce((sum, job) => sum + (job.duration || 0), 0),
totalCommission: techJobs.reduce((sum, job) => {
const gross = (job.income || 0) - (job.partsCost || 0);
if (job.paymentType === 'flat') {
return sum + (job.flatRateAmount || 0);
} else {
return sum + (gross * ((job.commissionRate || 0) / 100));
}
}, 0),
avgSatisfaction: techJobs.length > 0 ?
techJobs.reduce((sum, job) => sum + parseFloat(job.satisfaction || 3), 0) / techJobs.length : 3,
lastJob: techJobs.length > 0 ?
techJobs.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt))[0].completedAt : new Date().toISOString()
});
} else {
technicians[techIndex].jobs = techJobs.length;
technicians[techIndex].totalIncome = techJobs.reduce((sum, job) => sum + (job.income || 0), 0);
technicians[techIndex].totalHours = techJobs.reduce((sum, job) => sum + (job.duration || 0), 0);
technicians[techIndex].totalCommission = techJobs.reduce((sum, job) => {
const gross = (job.income || 0) - (job.partsCost || 0);
if (job.paymentType === 'flat') {
return sum + (job.flatRateAmount || 0);
} else {
return sum + (gross * ((job.commissionRate || 0) / 100));
}
}, 0);
technicians[techIndex].avgSatisfaction = techJobs.length > 0 ?
techJobs.reduce((sum, job) => sum + parseFloat(job.satisfaction || 3), 0) / techJobs.length : 3;
technicians[techIndex].lastJob = techJobs.length > 0 ?
techJobs.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt))[0].completedAt : new Date().toISOString();
}
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
}
// Update performance stats
function updatePerformanceStats() {
const totalLeads = leads.length;
const completedJobs = jobs.filter(j => {
const lead = leads.find(l => l.id === j.leadId);
return lead && lead.status === 'Completed';
}).length;
const conversionRate = totalLeads > 0 ? (completedJobs / totalLeads * 100) : 0;
const totalIncome = jobs.reduce((sum, job) => sum + (job.income || 0), 0);
const avgIncome = completedJobs > 0 ? totalIncome / completedJobs : 0;
// Calculate average hourly rate
let totalNetProfit = 0;
let totalHours = 0;
jobs.forEach(job => {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
totalNetProfit += netProfit;
totalHours += job.duration || 0;
});
const avgHourly = totalHours > 0 ? totalNetProfit / totalHours : 0;
document.getElementById('total-leads').textContent = totalLeads;
document.getElementById('conversion-rate').textContent = `${conversionRate.toFixed(1)}%`;
document.getElementById('avg-income').textContent = `$${avgIncome.toFixed(0)}`;
document.getElementById('avg-hourly').textContent = `$${avgHourly.toFixed(0)}/hr`;
// Update jobs table
updateJobsTable();
}
// Update jobs table
function updateJobsTable() {
const tableBody = document.querySelector('#jobs-table tbody');
tableBody.innerHTML = '';
const sortedJobs = jobs.slice().sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
sortedJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const jobDate = new Date(job.completedAt).toLocaleDateString();
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
const row = document.createElement('tr');
row.innerHTML = `
<td>${jobDate}</td>
<td>${lead.name}</td>
<td>${lead.notes}</td>
<td>$${(job.income || 0).toFixed(2)}</td>
<td>$${netProfit.toFixed(2)}</td>
<td>${job.satisfaction || '-'}/5</td>
<td>${job.review || 'No'}</td>
<td>${job.duration || 0}</td>
<td>${job.technician || 'N/A'}</td>
<td>$${techCommission.toFixed(2)}</td>
<td>$${(job.partsCost || 0).toFixed(2)}</td>
<td>${lead.status}</td>
`;
tableBody.appendChild(row);
});
// Update job totals
updateJobTotals();
}
// Update job totals
function updateJobTotals() {
const completedJobs = jobs.filter(j => {
const lead = leads.find(l => l.id === j.leadId);
return lead && lead.status === 'Completed';
});
const totalJobs = completedJobs.length;
const totalIncome = completedJobs.reduce((sum, job) => sum + (job.income || 0), 0);
const totalGrossProfit = completedJobs.reduce((sum, job) => sum + ((job.income || 0) - (job.partsCost || 0)), 0);
const totalDuration = completedJobs.reduce((sum, job) => sum + (job.duration || 0), 0);
const totalPartsCost = completedJobs.reduce((sum, job) => sum + (job.partsCost || 0), 0);
const totalReviews = completedJobs.filter(job => job.review === 'Yes').length;
let totalNetProfit = 0;
completedJobs.forEach(job => {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
totalNetProfit += netProfit;
});
const avgSale = totalJobs > 0 ? totalIncome / totalJobs : 0;
const avgProfit = totalJobs > 0 ? totalNetProfit / totalJobs : 0;
const avgSatisfaction = totalJobs > 0 ?
completedJobs.reduce((sum, job) => sum + parseFloat(job.satisfaction || 3), 0) / totalJobs : 0;
const avgParts = totalJobs > 0 ? totalPartsCost / totalJobs : 0;
document.getElementById('total-jobs').textContent = totalJobs;
document.getElementById('total-income').textContent = `$${totalIncome.toFixed(0)}`;
document.getElementById('total-gross-profit').textContent = `$${totalGrossProfit.toFixed(0)}`;
document.getElementById('avg-sale').textContent = `$${avgSale.toFixed(0)}`;
document.getElementById('total-duration').textContent = totalDuration.toFixed(1);
document.getElementById('total-profit').textContent = `$${totalNetProfit.toFixed(0)}`;
document.getElementById('avg-profit').textContent = `$${avgProfit.toFixed(0)}`;
document.getElementById('avg-satisfaction').textContent = avgSatisfaction.toFixed(1);
document.getElementById('total-parts').textContent = `$${totalPartsCost.toFixed(0)}`;
document.getElementById('avg-parts').textContent = `$${avgParts.toFixed(0)}`;
document.getElementById('total-reviews').textContent = totalReviews;
}
// Sort table function
function sortTable(columnIndex) {
const table = document.getElementById('jobs-table');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Toggle sort direction if clicking the same column
if (currentSortColumn === columnIndex) {
currentSortDirection *= -1;
} else {
currentSortColumn = columnIndex;
currentSortDirection = 1;
}
rows.sort((a, b) => {
const aCell = a.cells[columnIndex].textContent;
const bCell = b.cells[columnIndex].textContent;
let aValue, bValue;
// Parse values based on content type
if (aCell.startsWith('$')) {
aValue = parseFloat(aCell.replace(/[$,]/g, '')) || 0;
bValue = parseFloat(bCell.replace(/[$,]/g, '')) || 0;
} else if (columnIndex === 0) {
// FIX: date column must sort chronologically, not by month number
aValue = new Date(aCell).getTime() || 0;
bValue = new Date(bCell).getTime() || 0;
} else if (aCell.includes('/')) {
aValue = parseFloat(aCell.split('/')[0]) || 0;
bValue = parseFloat(bCell.split('/')[0]) || 0;
} else {
aValue = isNaN(aCell) ? aCell.toLowerCase() : parseFloat(aCell);
bValue = isNaN(bCell) ? bCell.toLowerCase() : parseFloat(bCell);
}
if (aValue < bValue) return -1 * currentSortDirection;
if (aValue > bValue) return 1 * currentSortDirection;
return 0;
});
// Clear and re-append sorted rows
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
// Update sort indicators
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
header.innerHTML = header.innerHTML.replace(/<i class="fas fa-sort-(up|down)"><\/i>/, '');
if (index === columnIndex) {
const iconClass = currentSortDirection === 1 ? 'fa-sort-up' : 'fa-sort-down';
header.innerHTML += ` <i class="fas ${iconClass}"></i>`;
}
});
}
// Filter jobs by date
function filterJobsByDate() {
const startDate = document.getElementById('job-start-date').value;
const endDate = document.getElementById('job-end-date').value;
if (!startDate || !endDate) {
alert('Please select both start and end dates');
return;
}
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include entire end date
const filteredJobs = jobs.filter(job => {
const jobDate = new Date(job.completedAt);
return jobDate >= start && jobDate <= end;
});
// Update table with filtered jobs
const tableBody = document.querySelector('#jobs-table tbody');
tableBody.innerHTML = '';
filteredJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const jobDate = new Date(job.completedAt).toLocaleDateString();
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
const row = document.createElement('tr');
row.innerHTML = `
<td>${jobDate}</td>
<td>${lead.name}</td>
<td>${lead.notes}</td>
<td>$${(job.income || 0).toFixed(2)}</td>
<td>$${netProfit.toFixed(2)}</td>
<td>${job.satisfaction || '-'}/5</td>
<td>${job.review || 'No'}</td>
<td>${job.duration || 0}</td>
<td>${job.technician || 'N/A'}</td>
<td>$${techCommission.toFixed(2)}</td>
<td>$${(job.partsCost || 0).toFixed(2)}</td>
<td>${lead.status}</td>
`;
tableBody.appendChild(row);
});
// Update totals for filtered jobs
updateFilteredJobTotals(filteredJobs);
}
// Update filtered job totals
function updateFilteredJobTotals(filteredJobs) {
const completedJobs = filteredJobs.filter(j => {
const lead = leads.find(l => l.id === j.leadId);
return lead && lead.status === 'Completed';
});
const totalJobs = completedJobs.length;
const totalIncome = completedJobs.reduce((sum, job) => sum + (job.income || 0), 0);
const totalGrossProfit = completedJobs.reduce((sum, job) => sum + ((job.income || 0) - (job.partsCost || 0)), 0);
const totalDuration = completedJobs.reduce((sum, job) => sum + (job.duration || 0), 0);
const totalPartsCost = completedJobs.reduce((sum, job) => sum + (job.partsCost || 0), 0);
const totalReviews = completedJobs.filter(job => job.review === 'Yes').length;
let totalNetProfit = 0;
completedJobs.forEach(job => {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
totalNetProfit += netProfit;
});
const avgSale = totalJobs > 0 ? totalIncome / totalJobs : 0;
const avgProfit = totalJobs > 0 ? totalNetProfit / totalJobs : 0;
const avgSatisfaction = totalJobs > 0 ?
completedJobs.reduce((sum, job) => sum + parseFloat(job.satisfaction || 3), 0) / totalJobs : 0;
const avgParts = totalJobs > 0 ? totalPartsCost / totalJobs : 0;
document.getElementById('total-jobs').textContent = totalJobs;
document.getElementById('total-income').textContent = `$${totalIncome.toFixed(0)}`;
document.getElementById('total-gross-profit').textContent = `$${totalGrossProfit.toFixed(0)}`;
document.getElementById('avg-sale').textContent = `$${avgSale.toFixed(0)}`;
document.getElementById('total-duration').textContent = totalDuration.toFixed(1);
document.getElementById('total-profit').textContent = `$${totalNetProfit.toFixed(0)}`;
document.getElementById('avg-profit').textContent = `$${avgProfit.toFixed(0)}`;
document.getElementById('avg-satisfaction').textContent = avgSatisfaction.toFixed(1);
document.getElementById('total-parts').textContent = `$${totalPartsCost.toFixed(0)}`;
document.getElementById('avg-parts').textContent = `$${avgParts.toFixed(0)}`;
document.getElementById('total-reviews').textContent = totalReviews;
}
// Reset job date filter
function resetJobDateFilter() {
document.getElementById('job-start-date').value = '';
document.getElementById('job-end-date').value = '';
updatePerformanceStats();
}
// Calculate lead cost
function calculateLeadCost() {
const marketingSpend = parseFloat(document.getElementById('marketing-spend').value) || 0;
const leadsGenerated = parseInt(document.getElementById('leads-generated').value) || 0;
const completedJobs = jobs.filter(j => {
const lead = leads.find(l => l.id === j.leadId);
return lead && lead.status === 'Completed';
}).length;
const costPerLead = leadsGenerated > 0 ? marketingSpend / leadsGenerated : 0;
const conversionRate = leadsGenerated > 0 ? (completedJobs / leadsGenerated * 100) : 0;
document.getElementById('cost-per-lead').textContent = `$${costPerLead.toFixed(2)}`;
document.getElementById('calculated-conversion-rate').textContent = `${conversionRate.toFixed(1)}%`;
let insight = '';
if (costPerLead > 100) {
insight = 'Your cost per lead is high. Consider optimizing your marketing channels.';
} else if (costPerLead > 50) {
insight = 'Moderate cost per lead. Look for opportunities to improve efficiency.';
} else if (costPerLead > 0) {
insight = 'Good cost per lead. Your marketing is efficient.';
} else {
insight = 'Enter your marketing data to see insights.';
}
document.getElementById('lead-cost-insight').textContent = insight;
}
// Export functions
function exportToPDF() {
alert('PDF export functionality would be implemented here with jsPDF');
// In a real implementation, this would use jsPDF to generate a PDF report
}
function exportToCSV() {
let csvContent = "Date,Customer,Service,Income,Profit,Satisfaction,Review,Duration,Technician,Commission,Parts Cost,Status\\n";
jobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const jobDate = new Date(job.completedAt).toLocaleDateString();
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
csvContent += `"${jobDate}","${lead.name}","${lead.notes}",${job.income || 0},${netProfit.toFixed(2)},${job.satisfaction || ''},"${job.review || 'No'}",${job.duration || 0},"${job.technician || ''}",${techCommission.toFixed(2)},${job.partsCost || 0},"${lead.status}"\\n`;
});
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('hidden', '');
a.setAttribute('href', url);
a.setAttribute('download', 'job_data.csv');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
function exportToGoogleSheets() {
alert('Google Sheets export would format data for easy import into Google Sheets');
// This could open a new window with formatted data or use Google Sheets API
exportToCSV(); // Fallback to CSV for now
}
// Initialize projection dates
function initializeProjectionDates() {
const today = new Date();
document.getElementById('start-date').valueAsDate = today;
const endDate = new Date(today);
endDate.setMonth(endDate.getMonth() + 1);
document.getElementById('end-date').valueAsDate = endDate;
}
// Calculate projections
function calculateProjections() {
const months = parseInt(document.getElementById('projection-months').value);
const growthRate = parseFloat(document.getElementById('growth-rate').value) / 100;
const strategyMultiplier = parseFloat(document.getElementById('strategy-multiplier').value);
const commissionRate = parseFloat(document.getElementById('commission-rate').value) / 100;
// Get baseline from recent performance (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentJobs = jobs.filter(job => {
const jobDate = new Date(job.completedAt);
return jobDate >= thirtyDaysAgo;
});
const monthlyJobs = recentJobs.length;
const monthlyIncome = recentJobs.reduce((sum, job) => sum + (job.income || 0), 0);
const monthlyProfit = recentJobs.reduce((sum, job) => {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
return sum + (netAfterTechnician - partnerCommission);
}, 0);
// Apply strategy multiplier
const adjustedMonthlyJobs = monthlyJobs * strategyMultiplier;
const adjustedMonthlyIncome = monthlyIncome * strategyMultiplier;
const adjustedMonthlyProfit = monthlyProfit * strategyMultiplier;
let projectionText = `Projection for ${months} months with ${(growthRate * 100).toFixed(1)}% monthly growth:\\n\\n`;
projectionText += `Current Monthly Performance:\\n`;
projectionText += `- Jobs: ${monthlyJobs}\\n`;
projectionText += `- Income: $${monthlyIncome.toFixed(2)}\\n`;
projectionText += `- Profit: $${monthlyProfit.toFixed(2)}\\n\\n`;
projectionText += `With ${strategyMultiplier}x Strategy:\\n`;
projectionText += `- Jobs: ${adjustedMonthlyJobs.toFixed(1)}\\n`;
projectionText += `- Income: $${adjustedMonthlyIncome.toFixed(2)}\\n`;
projectionText += `- Profit: $${adjustedMonthlyProfit.toFixed(2)}\\n\\n`;
projectionText += `Projected Growth:\\n`;
let totalJobs = 0;
let totalIncome = 0;
let totalProfit = 0;
let currentJobs = adjustedMonthlyJobs;
let currentIncome = adjustedMonthlyIncome;
let currentProfit = adjustedMonthlyProfit;
const labels = [];
const incomeData = [];
const profitData = [];
for (let i = 1; i <= months; i++) {
totalJobs += currentJobs;
totalIncome += currentIncome;
totalProfit += currentProfit;
if (i <= 12) { // Only show first 12 months in detail
projectionText += `Month ${i}: ${currentJobs.toFixed(1)} jobs, $${currentIncome.toFixed(2)} income, $${currentProfit.toFixed(2)} profit\\n`;
}
labels.push(`Month ${i}`);
incomeData.push(currentIncome);
profitData.push(currentProfit);
// Apply growth
currentJobs *= (1 + growthRate);
currentIncome *= (1 + growthRate);
currentProfit *= (1 + growthRate);
}
if (months > 12) {
projectionText += `...\\n`;
projectionText += `Month ${months}: ${currentJobs.toFixed(1)} jobs, $${currentIncome.toFixed(2)} income, $${currentProfit.toFixed(2)} profit\\n`;
}
projectionText += `\\nTotal Projected:\\n`;
projectionText += `- Jobs: ${totalJobs.toFixed(1)}\\n`;
projectionText += `- Income: $${totalIncome.toFixed(2)}\\n`;
projectionText += `- Profit: $${totalProfit.toFixed(2)}\\n`;
const roi = monthlyIncome > 0 ? (totalProfit / monthlyIncome) * 100 : 0;
projectionText += `- ROI: ${roi.toFixed(1)}%\\n`;
document.getElementById('projectionOutput').textContent = projectionText;
// Update metrics
document.getElementById('projected-income').textContent = `$${totalIncome.toFixed(0)}`;
document.getElementById('projected-profit').textContent = `$${totalProfit.toFixed(0)}`;
document.getElementById('projected-jobs').textContent = totalJobs.toFixed(0);
document.getElementById('projected-roi').textContent = `${roi.toFixed(1)}%`;
// Calculate 10x strategy impact
const tenXJobs = monthlyJobs * 10;
const tenXIncome = monthlyIncome * 10;
const tenXProfit = monthlyProfit * 10;
let tenXTotalJobs = 0;
let tenXTotalIncome = 0;
let tenXTotalProfit = 0;
let tenXCurrentJobs = tenXJobs;
let tenXCurrentIncome = tenXIncome;
let tenXCurrentProfit = tenXProfit;
for (let i = 1; i <= months; i++) {
tenXTotalJobs += tenXCurrentJobs;
tenXTotalIncome += tenXCurrentIncome;
tenXTotalProfit += tenXCurrentProfit;
tenXCurrentJobs *= (1 + growthRate);
tenXCurrentIncome *= (1 + growthRate);
tenXCurrentProfit *= (1 + growthRate);
}
document.getElementById('additional-income').textContent = `$${(tenXTotalIncome - totalIncome).toFixed(0)}`;
document.getElementById('additional-profit').textContent = `$${(tenXTotalProfit - totalProfit).toFixed(0)}`;
document.getElementById('additional-jobs').textContent = (tenXTotalJobs - totalJobs).toFixed(0);
document.getElementById('roi-improvement').textContent = `${((tenXTotalProfit / tenXTotalIncome) * 100 - roi).toFixed(1)}%`;
// Update projection chart
updateProjectionChart(labels, incomeData, profitData);
}
// Update projection chart
function updateProjectionChart(labels, incomeData, profitData) {
const ctx = document.getElementById('projectionChart').getContext('2d');
// Destroy existing chart if it exists
if (projectionChart) {
projectionChart.destroy();
}
projectionChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Projected Income',
data: incomeData,
borderColor: '#3498db',
backgroundColor: 'rgba(52, 152, 219, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Projected Profit',
data: profitData,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Business Projections'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value.toFixed(0);
}
}
}
}
}
});
}
// Update charts
function updateCharts() {
updateIncomeChart();
updateSourceChart();
updateProfitChart();
updateSatisfactionChart();
updateOfferChart();
}
// Update income chart
function updateIncomeChart() {
const ctx = document.getElementById('incomeChart').getContext('2d');
// Group jobs by month
const monthlyData = {};
jobs.forEach(job => {
const date = new Date(job.completedAt);
const monthYear = `${date.getFullYear()}-${date.getMonth() + 1}`;
if (!monthlyData[monthYear]) {
monthlyData[monthYear] = {
income: 0,
profit: 0,
jobs: 0
};
}
monthlyData[monthYear].income += job.income || 0;
// Calculate profit for this job
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
monthlyData[monthYear].profit += netProfit;
monthlyData[monthYear].jobs += 1;
});
const labels = Object.keys(monthlyData).sort();
const incomeData = labels.map(label => monthlyData[label].income);
const profitData = labels.map(label => monthlyData[label].profit);
// Destroy existing chart if it exists
if (incomeChart) {
incomeChart.destroy();
}
incomeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Income',
data: incomeData,
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: '#3498db',
borderWidth: 1
},
{
label: 'Profit',
data: profitData,
backgroundColor: 'rgba(46, 204, 113, 0.7)',
borderColor: '#2ecc71',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Monthly Income & Profit'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value;
}
}
}
}
}
});
}
// Update source chart
function updateSourceChart() {
const ctx = document.getElementById('sourceChart').getContext('2d');
// Count leads by source
const sourceCounts = {};
leads.forEach(lead => {
const source = lead.source || 'Unknown';
sourceCounts[source] = (sourceCounts[source] || 0) + 1;
});
const labels = Object.keys(sourceCounts);
const data = Object.values(sourceCounts);
// Destroy existing chart if it exists
if (sourceChart) {
sourceChart.destroy();
}
sourceChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#3498db',
'#2ecc71',
'#e74c3c',
'#f39c12',
'#9b59b6',
'#1abc9c',
'#34495e',
'#7f8c8d'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Lead Sources'
},
legend: {
position: 'right'
}
}
}
});
}
// Update profit chart
function updateProfitChart() {
const ctx = document.getElementById('profitChart').getContext('2d');
// Calculate profitability by job type/service
const serviceProfit = {};
jobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const service = lead.notes.substring(0, 20) + (lead.notes.length > 20 ? '...' : '');
// Calculate profit for this job
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
if (!serviceProfit[service]) {
serviceProfit[service] = {
totalProfit: 0,
count: 0
};
}
serviceProfit[service].totalProfit += netProfit;
serviceProfit[service].count += 1;
});
// Convert to arrays for chart
const labels = Object.keys(serviceProfit);
const data = labels.map(label => serviceProfit[label].totalProfit / serviceProfit[label].count);
// Destroy existing chart if it exists
if (profitChart) {
profitChart.destroy();
}
profitChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Average Profit per Job',
data: data,
backgroundColor: 'rgba(46, 204, 113, 0.7)',
borderColor: '#2ecc71',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Average Profit by Service Type'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value.toFixed(0);
}
}
}
}
}
});
}
// Update satisfaction chart
function updateSatisfactionChart() {
const ctx = document.getElementById('satisfactionChart').getContext('2d');
// Count satisfaction ratings
const satisfactionCounts = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0};
jobs.forEach(job => {
const satisfaction = parseInt(job.satisfaction) || 3;
if (satisfaction >= 1 && satisfaction <= 5) {
satisfactionCounts[satisfaction] = (satisfactionCounts[satisfaction] || 0) + 1;
}
});
const labels = ['1 - Very Dissatisfied', '2 - Dissatisfied', '3 - Neutral', '4 - Satisfied', '5 - Very Satisfied'];
const data = [satisfactionCounts[1], satisfactionCounts[2], satisfactionCounts[3], satisfactionCounts[4], satisfactionCounts[5]];
// Destroy existing chart if it exists
if (satisfactionChart) {
satisfactionChart.destroy();
}
satisfactionChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#e74c3c',
'#f39c12',
'#3498db',
'#2ecc71',
'#27ae60'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Customer Satisfaction Distribution'
},
legend: {
position: 'right'
}
}
}
});
}
// Update offer chart
function updateOfferChart() {
const ctx = document.getElementById('offerChart').getContext('2d');
// Count offers by status
const statusCounts = {
'Pending': 0,
'Sent': 0,
'Accepted': 0,
'Rejected': 0
};
offers.forEach(offer => {
statusCounts[offer.status] = (statusCounts[offer.status] || 0) + 1;
});
const labels = Object.keys(statusCounts);
const data = Object.values(statusCounts);
// Destroy existing chart if it exists
if (offerChart) {
offerChart.destroy();
}
offerChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Number of Offers',
data: data,
backgroundColor: [
'#f39c12',
'#3498db',
'#2ecc71',
'#e74c3c'
],
borderColor: [
'#d35400',
'#2980b9',
'#27ae60',
'#c0392b'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Offer Status Distribution'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
// Update customer list
function updateCustomerList() {
const listDiv = document.getElementById('customerList');
listDiv.innerHTML = '';
if (customers.length === 0) {
listDiv.innerHTML = '<p>No customers yet.</p>';
return;
}
customers.forEach(customer => {
const customerCard = document.createElement('div');
customerCard.className = 'customer-card';
customerCard.onclick = () => showCustomerDetails(customer);
const servicesText = customer.services && customer.services.length > 0 ?
customer.services.join(', ') : 'No services recorded';
customerCard.innerHTML = `
<h4>${customer.name}</h4>
<p><strong>Phone:</strong> ${customer.phone}</p>
<p><strong>Address:</strong> ${customer.address}</p>
<p><strong>Last Service:</strong> ${new Date(customer.lastServiceDate).toLocaleDateString()}</p>
<p><strong>Services:</strong> ${servicesText}</p>
<p><strong>Status:</strong> ${customer.status}</p>
`;
listDiv.appendChild(customerCard);
});
}
// Search customers
function searchCustomers() {
const searchTerm = document.getElementById('customer-search').value.toLowerCase();
const customerCards = document.querySelectorAll('.customer-card');
customerCards.forEach(card => {
const text = card.textContent.toLowerCase();
if (text.includes(searchTerm)) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
}
// Show customer details
function showCustomerDetails(customer) {
document.getElementById('customerDetails').style.display = 'block';
document.getElementById('customer-status').value = customer.status;
// Update customer detail content
let detailContent = `
<div class="sheet-row">
<div class="sheet-label">Name:</div>
<div class="sheet-value">${customer.name}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Phone:</div>
<div class="sheet-value">${customer.phone}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Address:</div>
<div class="sheet-value">${customer.address}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Email:</div>
<div class="sheet-value">${customer.email || 'N/A'}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">First Service:</div>
<div class="sheet-value">${new Date(customer.firstServiceDate).toLocaleDateString()}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Last Service:</div>
<div class="sheet-value">${new Date(customer.lastServiceDate).toLocaleDateString()}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Services:</div>
<div class="sheet-value">${customer.services ? customer.services.join(', ') : 'None'}</div>
</div>
`;
// Add additional customer details if available
if (customer.houseValue) {
detailContent += `
<div class="sheet-row">
<div class="sheet-label">House Value:</div>
<div class="sheet-value">${customer.houseValue}</div>
</div>
`;
}
if (customer.garageType) {
detailContent += `
<div class="sheet-row">
<div class="sheet-label">Garage Type:</div>
<div class="sheet-value">${customer.garageType}</div>
</div>
`;
}
document.getElementById('customerDetailContent').innerHTML = detailContent;
// Update job history
updateCustomerJobHistory(customer);
// Update offer history
updateCustomerOfferHistory(customer);
}
// Update customer job history
function updateCustomerJobHistory(customer) {
const customerJobs = jobs.filter(job => {
const lead = leads.find(l => l.id === job.leadId);
return lead && lead.phone === customer.phone;
});
let jobHistoryContent = '';
if (customerJobs.length === 0) {
jobHistoryContent = '<p>No job history for this customer.</p>';
} else {
customerJobs.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
customerJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
jobHistoryContent += `
<div class="job-item">
<div class="job-header">
<span>${new Date(job.completedAt).toLocaleDateString()}</span>
<span>$${(job.income || 0).toFixed(2)}</span>
</div>
<p><strong>Service:</strong> ${lead.notes}</p>
<p><strong>Technician:</strong> ${job.technician || 'N/A'}</p>
<p><strong>Satisfaction:</strong> ${job.satisfaction || '-'}/5</p>
<p><strong>Review:</strong> ${job.review || 'No'}</p>
</div>
`;
});
}
document.getElementById('jobHistoryContent').innerHTML = jobHistoryContent;
}
// Update customer offer history
function updateCustomerOfferHistory(customer) {
const customerOffers = offers.filter(offer => offer.customerPhone === customer.phone);
let offerHistoryContent = '';
if (customerOffers.length === 0) {
offerHistoryContent = '<p>No offer history for this customer.</p>';
} else {
customerOffers.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
customerOffers.forEach(offer => {
offerHistoryContent += `
<div class="offer-item-card">
<div class="offer-header">
<span>${new Date(offer.createdAt).toLocaleDateString()}</span>
<span>${offer.status}</span>
</div>
<p><strong>Subject:</strong> ${offer.subject}</p>
<p><strong>Services:</strong> ${offer.services ? offer.services.join(', ') : 'N/A'}</p>
<p><strong>Message:</strong> ${offer.message || 'No custom message'}</p>
<button class="offer-edit-btn" onclick="openEditOfferModal(${offer.id})"><i class="fas fa-edit"></i> <span>Edit</span></button>
<button class="offer-delete-btn danger-button" onclick="deleteOffer(${offer.id})"><i class="fas fa-trash"></i> <span>Delete</span></button>
</div>
`;
});
}
document.getElementById('offerHistoryContent').innerHTML = offerHistoryContent;
}
// Update customer status
function updateCustomerStatus() {
// This would update the customer status in the database
// For now, we'll just show an alert
const newStatus = document.getElementById('customer-status').value;
alert(`Customer status updated to: ${newStatus}`);
}
// Prepare offer for customer
function prepareOffer() {
// This would navigate to the offers tab and pre-select the customer
openTab('offers');
// Implementation would depend on how customer selection works in offers tab
}
// Contact customer
function contactCustomer() {
alert('This would initiate customer contact (call, email, etc.)');
}
// Edit customer
function editCustomer() {
// Get the current customer from the displayed details
const customerName = document.querySelector('#customerDetailContent .sheet-row:first-child .sheet-value').textContent;
const customer = customers.find(c => c.name === customerName);
if (customer) {
openEditCustomerModal(customer);
}
}
// Delete customer
function deleteCustomer() {
if (confirm('Are you sure you want to delete this customer? This action cannot be undone.')) {
// Get the current customer from the displayed details
const customerName = document.querySelector('#customerDetailContent .sheet-row:first-child .sheet-value').textContent;
const customerIndex = customers.findIndex(c => c.name === customerName);
if (customerIndex !== -1) {
customers.splice(customerIndex, 1);
localStorage.setItem('garageDoorCustomers', JSON.stringify(customers));
// Also delete associated leads
const customerLeads = leads.filter(l => l.name === customerName);
customerLeads.forEach(lead => {
const leadIndex = leads.findIndex(l => l.id === lead.id);
if (leadIndex !== -1) {
leads.splice(leadIndex, 1);
}
});
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
alert('Customer deleted successfully!');
updateCustomerList();
document.getElementById('customerDetails').style.display = 'none';
}
}
}
// Update customer dropdown for offers
function updateCustomerDropdown() {
const select = document.getElementById('offer-customer');
select.innerHTML = '<option value="">-- Select a customer --</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.phone;
option.textContent = `${customer.name} - ${customer.phone}`;
select.appendChild(option);
});
// Also update followup customer dropdown
const followupSelect = document.getElementById('followup-customer');
if (followupSelect) {
followupSelect.innerHTML = '<option value="">-- Select a customer --</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.phone;
option.textContent = `${customer.name} - ${customer.phone}`;
followupSelect.appendChild(option);
});
}
}
// Load customer for offer
function loadCustomerForOffer() {
const customerPhone = document.getElementById('offer-customer').value;
if (customerPhone) {
document.getElementById('offerForm').style.display = 'block';
} else {
document.getElementById('offerForm').style.display = 'none';
}
}
// Toggle offer dropdown
function toggleOfferDropdown() {
const dropdown = document.getElementById('offerDropdownContent');
dropdown.classList.toggle('show');
}
// Generate offer dropdown content
function generateOfferDropdown() {
const dropdown = document.getElementById('offerDropdownContent');
dropdown.innerHTML = '';
offerCategories.forEach(category => {
const categoryHeader = document.createElement('div');
categoryHeader.style.padding = '10px';
categoryHeader.style.fontWeight = 'bold';
categoryHeader.style.backgroundColor = '#f0f0f0';
categoryHeader.style.borderBottom = '1px solid var(--border-color)';
categoryHeader.textContent = category.name;
dropdown.appendChild(categoryHeader);
category.items.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.className = 'offer-dropdown-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = item;
checkbox.id = `offer-${category.id}-${item.replace(/\s+/g, '-').toLowerCase()}`;
const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.textContent = item;
label.style.marginLeft = '5px';
label.style.cursor = 'pointer';
itemDiv.appendChild(checkbox);
itemDiv.appendChild(label);
dropdown.appendChild(itemDiv);
});
});
}
// Generate offer
function generateOffer() {
const customerPhone = document.getElementById('offer-customer').value;
const customer = customers.find(c => c.phone === customerPhone);
const subject = document.getElementById('offer-subject').value;
const message = document.getElementById('offer-message').value;
const status = document.getElementById('offer-status').value;
if (!customer) {
alert('Please select a customer first');
return;
}
// Get selected services
const selectedServices = [];
const checkboxes = document.querySelectorAll('#offerDropdownContent input[type="checkbox"]:checked');
checkboxes.forEach(checkbox => {
selectedServices.push(checkbox.value);
});
if (selectedServices.length === 0) {
alert('Please select at least one service to include in the offer');
return;
}
let offerText = `Future Offer for ${customer.name}\n\n`;
offerText += `Subject: ${subject}\n\n`;
offerText += `Dear ${customer.name},\n\n`;
offerText += `We'd like to offer you the following services:\n\n`;
selectedServices.forEach(service => {
offerText += `• ${service}\n`;
});
offerText += `\n`;
if (message) {
offerText += `${message}\n\n`;
}
offerText += `Best regards,\nYour Garage Door Service Team`;
document.getElementById('offerOutput').textContent = offerText;
document.getElementById('offerOutput').style.display = 'block';
document.getElementById('offerShareButtons').style.display = 'block';
}
// Create new offer
function createNewOffer() {
const customerPhone = document.getElementById('offer-customer').value;
const customer = customers.find(c => c.phone === customerPhone);
const subject = document.getElementById('offer-subject').value;
const message = document.getElementById('offer-message').value;
const status = document.getElementById('offer-status').value;
if (!customer) {
alert('Please select a customer first');
return;
}
// Get selected services
const selectedServices = [];
const checkboxes = document.querySelectorAll('#offerDropdownContent input[type="checkbox"]:checked');
checkboxes.forEach(checkbox => {
selectedServices.push(checkbox.value);
});
if (selectedServices.length === 0) {
alert('Please select at least one service to include in the offer');
return;
}
const offer = {
id: Date.now(),
customerName: customer.name,
customerPhone: customer.phone,
subject: subject,
services: selectedServices,
message: message,
status: status,
createdAt: new Date().toISOString()
};
offers.push(offer);
localStorage.setItem('garageDoorOffers', JSON.stringify(offers));
alert('Offer created successfully!');
updateAllOffersList();
// Reset form
document.getElementById('offer-subject').value = '';
document.getElementById('offer-message').value = '';
document.getElementById('offer-status').value = 'Pending';
// Uncheck all checkboxes
const checkboxesAll = document.querySelectorAll('#offerDropdownContent input[type="checkbox"]');
checkboxesAll.forEach(checkbox => {
checkbox.checked = false;
});
document.getElementById('selected-offers-text').textContent = 'Select services...';
}
// Share offer functions
function shareOfferViaWhatsApp() {
const offerText = document.getElementById('offerOutput').textContent;
window.open(`https://wa.me/?text=${encodeURIComponent(offerText)}`);
}
function copyOfferToClipboard() {
const offerText = document.getElementById('offerOutput').textContent;
navigator.clipboard.writeText(offerText).then(() => {
alert('Offer copied to clipboard!');
});
}
function shareOfferViaEmail() {
const offerText = document.getElementById('offerOutput').textContent;
window.open(`mailto:?subject=Future Service Offer&body=${encodeURIComponent(offerText)}`);
}
function saveOffer() {
alert('Offer saved to database!');
// The offer is already saved when created, this is just for the generated preview
}
// Update all offers list
function updateAllOffersList() {
const listDiv = document.getElementById('allOffersList');
listDiv.innerHTML = '';
if (offers.length === 0) {
listDiv.innerHTML = '<p>No offers created yet.</p>';
return;
}
const sortedOffers = offers.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
sortedOffers.forEach(offer => {
const offerCard = document.createElement('div');
offerCard.className = 'offer-item-card';
offerCard.innerHTML = `
<div class="offer-header">
<span>${offer.customerName}</span>
<span>${offer.status}</span>
</div>
<p><strong>Subject:</strong> ${offer.subject}</p>
<p><strong>Date:</strong> ${new Date(offer.createdAt).toLocaleDateString()}</p>
<div class="offer-services">
<strong>Services:</strong>
<ul>
${offer.services.map(service => `<li>${service}</li>`).join('')}
</ul>
</div>
<p><strong>Message:</strong> ${offer.message || 'No custom message'}</p>
<button class="offer-edit-btn" onclick="openEditOfferModal(${offer.id})"><i class="fas fa-edit"></i> <span>Edit</span></button>
<button class="offer-delete-btn danger-button" onclick="deleteOffer(${offer.id})"><i class="fas fa-trash"></i> <span>Delete</span></button>
`;
listDiv.appendChild(offerCard);
});
}
// Edit offer modal functions
function openEditOfferModal(offerId) {
const offer = offers.find(o => o.id === offerId);
if (!offer) return;
document.getElementById('edit-offer-id').value = offerId;
document.getElementById('edit-offer-subject').value = offer.subject;
document.getElementById('edit-offer-message').value = offer.message || '';
document.getElementById('edit-offer-status').value = offer.status;
document.getElementById('editOfferModal').style.display = 'block';
}
function closeEditOfferModal() {
document.getElementById('editOfferModal').style.display = 'none';
}
function saveEditedOffer() {
const offerId = parseInt(document.getElementById('edit-offer-id').value);
const offerIndex = offers.findIndex(o => o.id === offerId);
if (offerIndex === -1) {
alert('Offer not found');
return;
}
offers[offerIndex].subject = document.getElementById('edit-offer-subject').value;
offers[offerIndex].message = document.getElementById('edit-offer-message').value;
offers[offerIndex].status = document.getElementById('edit-offer-status').value;
localStorage.setItem('garageDoorOffers', JSON.stringify(offers));
closeEditOfferModal();
updateAllOffersList();
updateCustomerOfferHistory(customers.find(c => c.phone === offers[offerIndex].customerPhone));
alert('Offer updated successfully!');
}
// Delete offer
function deleteOffer(offerId) {
if (confirm('Are you sure you want to delete this offer? This action cannot be undone.')) {
const offerIndex = offers.findIndex(o => o.id === offerId);
if (offerIndex !== -1) {
offers.splice(offerIndex, 1);
localStorage.setItem('garageDoorOffers', JSON.stringify(offers));
updateAllOffersList();
alert('Offer deleted successfully!');
}
}
}
// Add technician
function addTechnician() {
const name = document.getElementById('new-tech-name').value.trim();
if (!name) {
alert('Please enter a technician name');
return;
}
if (technicians.some(t => t.name === name)) {
alert('Technician already exists');
return;
}
technicians.push({
name: name,
jobs: 0,
totalIncome: 0,
totalHours: 0,
totalCommission: 0,
avgSatisfaction: 0,
lastJob: null
});
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
document.getElementById('new-tech-name').value = '';
updateTechniciansList();
updateTechnicianDropdown();
alert('Technician added successfully!');
}
// Update technicians list
function updateTechniciansList() {
const listDiv = document.getElementById('techniciansList');
listDiv.innerHTML = '';
if (technicians.length === 0) {
listDiv.innerHTML = '<p>No technicians added yet.</p>';
return;
}
technicians.forEach(tech => {
const techCard = document.createElement('div');
techCard.className = 'tech-card-item';
techCard.onclick = () => showTechnicianDetails(tech.name);
const lastJobText = tech.lastJob ?
new Date(tech.lastJob).toLocaleDateString() : 'No jobs yet';
techCard.innerHTML = `
<h4>${tech.name}</h4>
<p><strong>Jobs Completed:</strong> ${tech.jobs}</p>
<p><strong>Total Commission:</strong> $${(tech.totalCommission || 0).toFixed(2)}</p>
<p><strong>Average Satisfaction:</strong> ${(tech.avgSatisfaction || 0).toFixed(1)}/5</p>
<p><strong>Last Job:</strong> ${lastJobText}</p>
`;
listDiv.appendChild(techCard);
});
}
// Show technician details
function showTechnicianDetails(techName) {
currentTechnicianName = techName;
const tech = technicians.find(t => t.name === techName);
if (!tech) return;
document.getElementById('technicianDetails').style.display = 'block';
// Update basic tech details
let techDetailContent = `
<div class="tech-stats">
<div class="tech-stat">
<div class="tech-stat-value">${tech.jobs}</div>
<div class="tech-stat-label">Total Jobs</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">$${(tech.totalIncome || 0).toFixed(0)}</div>
<div class="tech-stat-label">Total Income</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">$${(tech.totalCommission || 0).toFixed(0)}</div>
<div class="tech-stat-label">Total Commission</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">${(tech.avgSatisfaction || 0).toFixed(1)}</div>
<div class="tech-stat-label">Avg Satisfaction</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">${(tech.totalHours || 0).toFixed(1)}</div>
<div class="tech-stat-label">Total Hours</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">$${tech.totalHours > 0 ? (tech.totalCommission / tech.totalHours).toFixed(2) : '0'}</div>
<div class="tech-stat-label">Hourly Rate</div>
</div>
</div>
`;
document.getElementById('techDetailContent').innerHTML = techDetailContent;
// Update enhanced technician details if available
if (techDetails[techName]) {
const details = techDetails[techName];
document.getElementById('tech-email').value = details.email || '';
document.getElementById('tech-phone').value = details.phone || '';
document.getElementById('tech-address').value = details.address || '';
document.getElementById('tech-emergency-contact').value = details.emergencyContact || '';
document.getElementById('tech-experience').value = details.experience || '';
document.getElementById('tech-certifications').value = details.certifications || '';
document.getElementById('tech-specializations').value = details.specializations || '';
document.getElementById('tech-training').value = details.training || '';
document.getElementById('tech-skills').value = details.skills || '';
document.getElementById('tech-equipment').value = details.equipment || '';
document.getElementById('tech-vehicle').value = details.vehicle || '';
document.getElementById('tech-insurance').value = details.insurance || '';
document.getElementById('tech-hours').value = details.hours || '';
document.getElementById('tech-max-jobs').value = details.maxJobs || 4;
document.getElementById('tech-service-radius').value = details.serviceRadius || 25;
document.getElementById('tech-job-preferences').value = details.jobPreferences || '';
document.getElementById('tech-base-rate').value = details.baseRate || '';
document.getElementById('tech-commission-structure').value = details.commissionStructure || '';
document.getElementById('tech-performance-goals').value = details.performanceGoals || '';
document.getElementById('tech-bonus-eligible').value = details.bonusEligible || 'Yes';
document.getElementById('tech-general-notes').value = details.generalNotes || '';
document.getElementById('tech-strengths').value = details.strengths || '';
document.getElementById('tech-improvements').value = details.improvements || '';
} else {
// Reset all fields if no details exist
const fields = [
'tech-email', 'tech-phone', 'tech-address', 'tech-emergency-contact',
'tech-experience', 'tech-certifications', 'tech-specializations', 'tech-training',
'tech-skills', 'tech-equipment', 'tech-vehicle', 'tech-insurance',
'tech-hours', 'tech-max-jobs', 'tech-service-radius', 'tech-job-preferences',
'tech-base-rate', 'tech-commission-structure', 'tech-performance-goals',
'tech-bonus-eligible', 'tech-general-notes', 'tech-strengths', 'tech-improvements'
];
fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
if (element.type === 'select-one') {
element.value = '';
} else {
element.value = '';
}
}
});
// Set defaults
document.getElementById('tech-max-jobs').value = 4;
document.getElementById('tech-service-radius').value = 25;
document.getElementById('tech-bonus-eligible').value = 'Yes';
}
// Update job history
updateTechnicianJobHistory(techName);
}
// Save technician details
function saveTechnicianDetails() {
if (!currentTechnicianName) return;
const details = {
email: document.getElementById('tech-email').value,
phone: document.getElementById('tech-phone').value,
address: document.getElementById('tech-address').value,
emergencyContact: document.getElementById('tech-emergency-contact').value,
experience: document.getElementById('tech-experience').value,
certifications: document.getElementById('tech-certifications').value,
specializations: document.getElementById('tech-specializations').value,
training: document.getElementById('tech-training').value,
skills: document.getElementById('tech-skills').value,
equipment: document.getElementById('tech-equipment').value,
vehicle: document.getElementById('tech-vehicle').value,
insurance: document.getElementById('tech-insurance').value,
hours: document.getElementById('tech-hours').value,
maxJobs: parseInt(document.getElementById('tech-max-jobs').value) || 4,
serviceRadius: parseInt(document.getElementById('tech-service-radius').value) || 25,
jobPreferences: document.getElementById('tech-job-preferences').value,
baseRate: document.getElementById('tech-base-rate').value,
commissionStructure: document.getElementById('tech-commission-structure').value,
performanceGoals: document.getElementById('tech-performance-goals').value,
bonusEligible: document.getElementById('tech-bonus-eligible').value,
generalNotes: document.getElementById('tech-general-notes').value,
strengths: document.getElementById('tech-strengths').value,
improvements: document.getElementById('tech-improvements').value
};
techDetails[currentTechnicianName] = details;
localStorage.setItem('garageDoorTechDetails', JSON.stringify(techDetails));
alert('Technician details saved successfully!');
}
// Reset technician details
function resetTechnicianDetails() {
if (!currentTechnicianName) return;
if (confirm('Are you sure you want to reset all technician details? This cannot be undone.')) {
delete techDetails[currentTechnicianName];
localStorage.setItem('garageDoorTechDetails', JSON.stringify(techDetails));
// Clear all form fields
const fields = [
'tech-email', 'tech-phone', 'tech-address', 'tech-emergency-contact',
'tech-experience', 'tech-certifications', 'tech-specializations', 'tech-training',
'tech-skills', 'tech-equipment', 'tech-vehicle', 'tech-insurance',
'tech-hours', 'tech-job-preferences', 'tech-base-rate', 'tech-commission-structure',
'tech-performance-goals', 'tech-general-notes', 'tech-strengths', 'tech-improvements'
];
fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) element.value = '';
});
// Reset defaults
document.getElementById('tech-max-jobs').value = 4;
document.getElementById('tech-service-radius').value = 25;
document.getElementById('tech-bonus-eligible').value = 'Yes';
alert('Technician details reset successfully!');
}
}
// Delete technician details
function deleteTechnicianDetails() {
if (!currentTechnicianName) return;
if (confirm('Are you sure you want to delete all details for this technician? This cannot be undone.')) {
delete techDetails[currentTechnicianName];
localStorage.setItem('garageDoorTechDetails', JSON.stringify(techDetails));
// Also remove the technician from the list
const techIndex = technicians.findIndex(t => t.name === currentTechnicianName);
if (techIndex !== -1) {
technicians.splice(techIndex, 1);
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
}
alert('Technician details deleted successfully!');
updateTechniciansList();
updateTechnicianDropdown();
document.getElementById('technicianDetails').style.display = 'none';
}
}
// Update technician job history
function updateTechnicianJobHistory(techName) {
const techJobs = jobs.filter(job => job.technician === techName);
let jobHistoryContent = '';
if (techJobs.length === 0) {
jobHistoryContent = '<p>No job history for this technician.</p>';
} else {
techJobs.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
techJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
jobHistoryContent += `
<div class="job-item">
<div class="job-header">
<span>${new Date(job.completedAt).toLocaleDateString()}</span>
<span>$${(job.income || 0).toFixed(2)}</span>
</div>
<p><strong>Customer:</strong> ${lead.name}</p>
<p><strong>Service:</strong> ${lead.notes}</p>
<p><strong>Commission:</strong> $${techCommission.toFixed(2)}</p>
<p><strong>Duration:</strong> ${job.duration || 0} hours</p>
<p><strong>Satisfaction:</strong> ${job.satisfaction || '-'}/5</p>
</div>
`;
});
}
document.getElementById('techJobHistoryContent').innerHTML = jobHistoryContent;
}
// Edit technician
function editTechnician() {
alert('Edit technician functionality would be implemented here');
// This would open a modal or form to edit technician details
}
// Delete technician
function deleteTechnician() {
if (!currentTechnicianName) return;
if (confirm('Are you sure you want to delete this technician? This will remove all their data.')) {
const techIndex = technicians.findIndex(t => t.name === currentTechnicianName);
if (techIndex !== -1) {
technicians.splice(techIndex, 1);
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
// Also remove from tech details
delete techDetails[currentTechnicianName];
localStorage.setItem('garageDoorTechDetails', JSON.stringify(techDetails));
alert('Technician deleted successfully!');
updateTechniciansList();
updateTechnicianDropdown();
document.getElementById('technicianDetails').style.display = 'none';
}
}
}
// Update my account
function updateMyAccount() {
// For demo purposes, we'll use the first technician as "my" account
// In a real app, this would be based on logged-in user
const myTechName = technicians.length > 0 ? technicians[0].name : null;
if (!myTechName) {
document.getElementById('my-total-jobs').textContent = '0';
document.getElementById('my-total-commission').textContent = '$0';
document.getElementById('my-avg-hourly').textContent = '$0/hr';
document.getElementById('my-avg-satisfaction').textContent = '0/5';
document.getElementById('myJobsList').innerHTML = '<p>No jobs found.</p>';
return;
}
const myJobs = jobs.filter(job => job.technician === myTechName);
const paidJobs = myJobs.filter(job => job.paid);
const unpaidJobs = myJobs.filter(job => !job.paid);
const displayJobs = myAccountFilterPaid ? paidJobs : myJobs;
// Update stats
document.getElementById('my-total-jobs').textContent = myJobs.length;
document.getElementById('my-total-commission').textContent = `$${calculateMyTotalCommission(myJobs).toFixed(0)}`;
document.getElementById('my-avg-hourly').textContent = `$${calculateMyAvgHourly(myJobs).toFixed(0)}/hr`;
document.getElementById('my-avg-satisfaction').textContent = `${calculateMyAvgSatisfaction(myJobs).toFixed(1)}/5`;
// Update payment filter label
document.getElementById('payment-filter-label').textContent = myAccountFilterPaid ? 'Paid Jobs Only' : 'All Jobs';
// Update job list
updateMyJobsList(displayJobs);
}
// Calculate my total commission
function calculateMyTotalCommission(myJobs) {
return myJobs.reduce((total, job) => {
const gross = (job.income || 0) - (job.partsCost || 0);
if (job.paymentType === 'flat') {
return total + (job.flatRateAmount || 0);
} else {
return total + (gross * ((job.commissionRate || 0) / 100));
}
}, 0);
}
// Calculate my average hourly rate
function calculateMyAvgHourly(myJobs) {
const totalCommission = calculateMyTotalCommission(myJobs);
const totalHours = myJobs.reduce((total, job) => total + (job.duration || 0), 0);
return totalHours > 0 ? totalCommission / totalHours : 0;
}
// Calculate my average satisfaction
function calculateMyAvgSatisfaction(myJobs) {
const totalSatisfaction = myJobs.reduce((total, job) => total + parseFloat(job.satisfaction || 3), 0);
return myJobs.length > 0 ? totalSatisfaction / myJobs.length : 0;
}
// Update my jobs list
function updateMyJobsList(jobsToShow) {
const listDiv = document.getElementById('myJobsList');
listDiv.innerHTML = '';
if (jobsToShow.length === 0) {
listDiv.innerHTML = '<p>No jobs found.</p>';
return;
}
const sortedJobs = jobsToShow.slice().sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
sortedJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const hourlyRate = job.duration > 0 ? techCommission / job.duration : 0;
const paymentStatus = job.paid ? 'paid' : 'unpaid';
const jobCard = document.createElement('div');
jobCard.className = 'my-job-item';
jobCard.innerHTML = `
<div class="my-job-header">
<span>${new Date(job.completedAt).toLocaleDateString()}</span>
<span class="payment-status-badge ${paymentStatus}">${job.paid ? 'Paid' : 'Unpaid'}</span>
</div>
<p><strong>Customer:</strong> ${lead.name}</p>
<p><strong>Service:</strong> ${lead.notes}</p>
<p><strong>Commission:</strong> $${techCommission.toFixed(2)}</p>
<p><strong>Hourly Rate:</strong> $${hourlyRate.toFixed(2)}/hr</p>
<p><strong>Duration:</strong> ${job.duration || 0} hours</p>
<p><strong>Satisfaction:</strong> ${job.satisfaction || '-'}/5</p>
${!job.paid ? '<button onclick="markJobAsPaid(' + job.id + ')" class="success-button" style="margin-top: 10px;"><i class="fas fa-check"></i> <span>Mark as Paid</span></button>' : ''}
`;
listDiv.appendChild(jobCard);
});
}
// Toggle payment filter
function togglePaymentFilter() {
myAccountFilterPaid = !myAccountFilterPaid;
updateMyAccount();
}
// Mark job as paid
function markJobAsPaid(jobId) {
const jobIndex = jobs.findIndex(j => j.id === jobId);
if (jobIndex !== -1) {
jobs[jobIndex].paid = true;
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
updateMyAccount();
alert('Job marked as paid!');
}
}
// Add strategy
function addStrategy() {
const name = document.getElementById('new-strategy-name').value.trim();
if (!name) {
alert('Please enter a strategy name');
return;
}
if (strategies.some(s => s.name === name)) {
alert('Strategy already exists');
return;
}
strategies.push({
id: Date.now(),
name: name,
description: document.getElementById('strategy-description').value,
successRate: parseInt(document.getElementById('strategy-success-rate').value) || 50,
createdAt: new Date().toISOString()
});
localStorage.setItem('garageDoorStrategies', JSON.stringify(strategies));
document.getElementById('new-strategy-name').value = '';
document.getElementById('strategy-description').value = '';
document.getElementById('strategy-success-rate').value = '50';
updateStrategiesList();
alert('Strategy added successfully!');
}
// Save strategy
function saveStrategy() {
// This would save the currently edited strategy
// For now, we'll just use the add functionality
addStrategy();
}
// Update strategies list
function updateStrategiesList() {
const listDiv = document.getElementById('strategiesList');
listDiv.innerHTML = '';
if (strategies.length === 0) {
listDiv.innerHTML = '<p>No strategies added yet.</p>';
return;
}
strategies.forEach(strategy => {
const strategyCard = document.createElement('div');
strategyCard.className = 'strategy-card';
strategyCard.innerHTML = `
<div class="strategy-header">
<span>${strategy.name}</span>
<span>${strategy.successRate}% Success Rate</span>
</div>
<p>${strategy.description}</p>
<div class="strategy-actions">
<button onclick="openEditStrategyModal(${strategy.id})"><i class="fas fa-edit"></i> <span>Edit</span></button>
<button class="danger-button" onclick="deleteStrategy(${strategy.id})"><i class="fas fa-trash"></i> <span>Delete</span></button>
</div>
`;
listDiv.appendChild(strategyCard);
});
}
// Edit strategy modal functions
function openEditStrategyModal(strategyId) {
const strategy = strategies.find(s => s.id === strategyId);
if (!strategy) return;
document.getElementById('edit-strategy-id').value = strategyId;
document.getElementById('edit-strategy-name').value = strategy.name;
document.getElementById('edit-strategy-description').value = strategy.description;
document.getElementById('edit-strategy-success-rate').value = strategy.successRate;
document.getElementById('editStrategyModal').style.display = 'block';
}
function closeEditStrategyModal() {
document.getElementById('editStrategyModal').style.display = 'none';
}
function saveEditedStrategy() {
const strategyId = parseInt(document.getElementById('edit-strategy-id').value);
const strategyIndex = strategies.findIndex(s => s.id === strategyId);
if (strategyIndex === -1) {
alert('Strategy not found');
return;
}
strategies[strategyIndex].name = document.getElementById('edit-strategy-name').value;
strategies[strategyIndex].description = document.getElementById('edit-strategy-description').value;
strategies[strategyIndex].successRate = parseInt(document.getElementById('edit-strategy-success-rate').value) || 50;
localStorage.setItem('garageDoorStrategies', JSON.stringify(strategies));
closeEditStrategyModal();
updateStrategiesList();
alert('Strategy updated successfully!');
}
// Delete strategy
function deleteStrategy(strategyId) {
if (confirm('Are you sure you want to delete this strategy?')) {
const strategyIndex = strategies.findIndex(s => s.id === strategyId);
if (strategyIndex !== -1) {
strategies.splice(strategyIndex, 1);
localStorage.setItem('garageDoorStrategies', JSON.stringify(strategies));
updateStrategiesList();
alert('Strategy deleted successfully!');
}
}
}
// Add mistake
function addMistake() {
const name = document.getElementById('new-mistake-name').value.trim();
if (!name) {
alert('Please enter a mistake description');
return;
}
if (mistakes.some(m => m.name === name)) {
alert('Mistake already exists');
return;
}
mistakes.push({
id: Date.now(),
name: name,
solution: document.getElementById('mistake-solution').value,
frequency: parseInt(document.getElementById('mistake-frequency').value) || 3,
createdAt: new Date().toISOString()
});
localStorage.setItem('garageDoorMistakes', JSON.stringify(mistakes));
document.getElementById('new-mistake-name').value = '';
document.getElementById('mistake-solution').value = '';
document.getElementById('mistake-frequency').value = '3';
updateMistakesList();
alert('Mistake added successfully!');
}
// Save mistake
function saveMistake() {
// This would save the currently edited mistake
// For now, we'll just use the add functionality
addMistake();
}
// Update mistakes list
function updateMistakesList() {
const listDiv = document.getElementById('mistakesList');
listDiv.innerHTML = '';
if (mistakes.length === 0) {
listDiv.innerHTML = '<p>No mistakes added yet.</p>';
return;
}
mistakes.forEach(mistake => {
const frequencyText =
mistake.frequency === 1 ? 'Rare' :
mistake.frequency === 2 ? 'Occasional' :
mistake.frequency === 3 ? 'Moderate' :
mistake.frequency === 4 ? 'Frequent' : 'Very Common';
const mistakeCard = document.createElement('div');
mistakeCard.className = 'mistake-card';
mistakeCard.innerHTML = `
<div class="mistake-header">
<span>${mistake.name}</span>
<span>${frequencyText}</span>
</div>
<p><strong>Solution/Prevention:</strong> ${mistake.solution}</p>
<div class="mistake-actions">
<button onclick="openEditMistakeModal(${mistake.id})"><i class="fas fa-edit"></i> <span>Edit</span></button>
<button class="danger-button" onclick="deleteMistake(${mistake.id})"><i class="fas fa-trash"></i> <span>Delete</span></button>
</div>
`;
listDiv.appendChild(mistakeCard);
});
}
// Edit mistake modal functions
function openEditMistakeModal(mistakeId) {
const mistake = mistakes.find(m => m.id === mistakeId);
if (!mistake) return;
document.getElementById('edit-mistake-id').value = mistakeId;
document.getElementById('edit-mistake-name').value = mistake.name;
document.getElementById('edit-mistake-solution').value = mistake.solution;
document.getElementById('edit-mistake-frequency').value = mistake.frequency;
document.getElementById('editMistakeModal').style.display = 'block';
}
function closeEditMistakeModal() {
document.getElementById('editMistakeModal').style.display = 'none';
}
function saveEditedMistake() {
const mistakeId = parseInt(document.getElementById('edit-mistake-id').value);
const mistakeIndex = mistakes.findIndex(m => m.id === mistakeId);
if (mistakeIndex === -1) {
alert('Mistake not found');
return;
}
mistakes[mistakeIndex].name = document.getElementById('edit-mistake-name').value;
mistakes[mistakeIndex].solution = document.getElementById('edit-mistake-solution').value;
mistakes[mistakeIndex].frequency = parseInt(document.getElementById('edit-mistake-frequency').value) || 3;
localStorage.setItem('garageDoorMistakes', JSON.stringify(mistakes));
closeEditMistakeModal();
updateMistakesList();
alert('Mistake updated successfully!');
}
// Delete mistake
function deleteMistake(mistakeId) {
if (confirm('Are you sure you want to delete this mistake?')) {
const mistakeIndex = mistakes.findIndex(m => m.id === mistakeId);
if (mistakeIndex !== -1) {
mistakes.splice(mistakeIndex, 1);
localStorage.setItem('garageDoorMistakes', JSON.stringify(mistakes));
updateMistakesList();
alert('Mistake deleted successfully!');
}
}
}
// Calendar functions
function renderCalendar() {
const calendarDays = document.getElementById('calendar-days');
calendarDays.innerHTML = '';
const monthYear = document.getElementById('calendar-month-year');
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
monthYear.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`;
// Get first day of month and number of days
const firstDay = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1);
const lastDay = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDay = firstDay.getDay(); // 0 = Sunday, 1 = Monday, etc.
// Add empty cells for days before the first day of the month
for (let i = 0; i < startingDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day other-month';
calendarDays.appendChild(emptyDay);
}
// Add cells for each day of the month
const today = new Date();
today.setHours(0, 0, 0, 0);
for (let day = 1; day <= daysInMonth; day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
const currentDate = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), day);
if (currentDate.getTime() === today.getTime()) {
dayElement.classList.add('today');
}
dayElement.setAttribute('data-date', currentDate.toISOString().split('T')[0]);
dayElement.setAttribute('ondragover', 'event.preventDefault()');
dayElement.setAttribute('ondrop', 'dropLead(event)');
const dayNumber = document.createElement('div');
dayNumber.className = 'calendar-day-number';
dayNumber.textContent = day;
dayElement.appendChild(dayNumber);
// Add jobs for this day
const dayJobs = getJobsForDate(currentDate);
let totalProfit = 0;
dayJobs.forEach(job => {
const jobElement = document.createElement('div');
jobElement.className = `calendar-job status-${job.status.toLowerCase().replace(' ', '')}`;
jobElement.textContent = `${job.customerName}: ${job.service}`;
jobElement.setAttribute('draggable', 'true');
jobElement.setAttribute('ondragstart', 'dragLead(event)');
jobElement.setAttribute('data-job-id', job.id);
jobElement.setAttribute('onclick', `showJobDetails(${job.id})`);
dayElement.appendChild(jobElement);
// Calculate profit for total
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
totalProfit += netProfit;
});
// Add reminders for this day
const dayReminders = getRemindersForDate(currentDate);
dayReminders.forEach(reminder => {
const reminderElement = document.createElement('div');
reminderElement.className = `calendar-job reminder-${reminder.type}`;
reminderElement.textContent = `${reminder.customerName}: ${reminder.type}`;
reminderElement.setAttribute('onclick', `completeReminder(${reminder.id})`);
dayElement.appendChild(reminderElement);
});
// Show total profit if there are jobs
if (totalProfit > 0) {
const profitElement = document.createElement('div');
profitElement.className = 'calendar-day-profit';
profitElement.textContent = `$${totalProfit.toFixed(0)}`;
dayElement.appendChild(profitElement);
}
calendarDays.appendChild(dayElement);
}
}
// Get jobs for a specific date
function getJobsForDate(date) {
const dateStr = date.toISOString().split('T')[0];
const dayJobs = [];
jobs.forEach(job => {
const jobDate = new Date(job.completedAt).toISOString().split('T')[0];
if (jobDate === dateStr) {
const lead = leads.find(l => l.id === job.leadId);
if (lead) {
dayJobs.push({
id: job.id,
customerName: lead.name,
service: lead.notes,
status: lead.status,
income: job.income,
partsCost: job.partsCost,
commissionRate: job.commissionRate,
partnerCommissionRate: job.partnerCommissionRate,
paymentType: job.paymentType,
flatRateAmount: job.flatRateAmount,
partner: job.partner
});
}
}
});
return dayJobs;
}
// Get reminders for a specific date
function getRemindersForDate(date) {
const dateStr = date.toISOString().split('T')[0];
return reminders.filter(reminder => {
const reminderDate = new Date(reminder.date).toISOString().split('T')[0];
return reminderDate === dateStr && !reminder.completed;
});
}
// Calendar navigation
function previousMonth() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
renderCalendar();
}
function currentMonth() {
currentCalendarDate = new Date();
renderCalendar();
}
function nextMonth() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
renderCalendar();
}
// Drag and drop functions
function dragLead(event) {
draggedLead = event.target.getAttribute('data-job-id');
event.target.classList.add('dragging');
}
function dropLead(event) {
event.preventDefault();
if (draggedLead) {
const _day = event.target.closest('.calendar-day');
if (!_day) { draggedLead = null; return; } // FIX: dropped outside a day cell
const targetDate = _day.getAttribute('data-date');
// Update the job date
const jobIndex = jobs.findIndex(j => j.id == draggedLead);
if (jobIndex !== -1) {
const newDate = new Date(targetDate);
// Keep the original time
const originalDate = new Date(jobs[jobIndex].completedAt);
newDate.setHours(originalDate.getHours(), originalDate.getMinutes(), originalDate.getSeconds());
jobs[jobIndex].completedAt = newDate.toISOString();
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
// Also update the lead date
const lead = leads.find(l => l.id === jobs[jobIndex].leadId);
if (lead) {
lead.date = targetDate;
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
}
renderCalendar();
updatePerformanceStats();
updateCharts();
}
// Remove dragging class
const draggingElement = document.querySelector('.calendar-job.dragging');
if (draggingElement) {
draggingElement.classList.remove('dragging');
}
draggedLead = null;
}
}
// Show job details in modal
function showJobDetails(jobId) {
const job = jobs.find(j => j.id == jobId);
if (!job) return;
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = job.partner ? netAfterTechnician * ((job.partnerCommissionRate || 0) / 100) : 0;
const netProfit = netAfterTechnician - partnerCommission;
let detailsContent = `
<h3>Job Details</h3>
<div class="sheet-row">
<div class="sheet-label">Customer:</div>
<div class="sheet-value">${lead.name}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Service:</div>
<div class="sheet-value">${lead.notes}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Date:</div>
<div class="sheet-value">${new Date(job.completedAt).toLocaleDateString()}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Status:</div>
<div class="sheet-value">${lead.status}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Income:</div>
<div class="sheet-value">$${(job.income || 0).toFixed(2)}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Parts Cost:</div>
<div class="sheet-value">$${(job.partsCost || 0).toFixed(2)}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Gross Profit:</div>
<div class="sheet-value">$${gross.toFixed(2)}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Technician Commission:</div>
<div class="sheet-value">$${techCommission.toFixed(2)}</div>
</div>
${job.partner ? `
<div class="sheet-row">
<div class="sheet-label">Partner Commission (${job.partnerCommissionRate}% to ${job.partner}):</div>
<div class="sheet-value">$${partnerCommission.toFixed(2)}</div>
</div>
` : ''}
<div class="sheet-row">
<div class="sheet-label">Net Profit:</div>
<div class="sheet-value">$${netProfit.toFixed(2)}</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Duration:</div>
<div class="sheet-value">${job.duration || 0} hours</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Satisfaction:</div>
<div class="sheet-value">${job.satisfaction || '-'}/5</div>
</div>
<div class="sheet-row">
<div class="sheet-label">Review:</div>
<div class="sheet-value">${job.review || 'No'}</div>
</div>
`;
document.getElementById('jobDetailsContent').innerHTML = detailsContent;
document.getElementById('jobDetailsModal').style.display = 'block';
}
function closeJobDetailsModal() {
document.getElementById('jobDetailsModal').style.display = 'none';
}
// Add follow-up
function addFollowUp() {
document.getElementById('followUpModal').style.display = 'block';
// Set default date to today
document.getElementById('followup-date').valueAsDate = new Date();
}
function closeFollowUpModal() {
document.getElementById('followUpModal').style.display = 'none';
}
function saveFollowUp() {
const customerPhone = document.getElementById('followup-customer').value;
const customer = customers.find(c => c.phone === customerPhone);
const date = document.getElementById('followup-date').value;
const time = document.getElementById('followup-time').value;
const notes = document.getElementById('followup-notes').value;
if (!customer) {
alert('Please select a customer');
return;
}
if (!date) {
alert('Please select a date');
return;
}
// Combine date and time
let dateTime = new Date(date);
if (time) {
const [hours, minutes] = time.split(':');
dateTime.setHours(parseInt(hours), parseInt(minutes));
}
const followup = {
id: Date.now(),
customerName: customer.name,
customerPhone: customer.phone,
date: dateTime.toISOString(),
notes: notes,
completed: false,
createdAt: new Date().toISOString()
};
followups.push(followup);
localStorage.setItem('garageDoorFollowups', JSON.stringify(followups));
closeFollowUpModal();
renderCalendar();
updateUpcomingReminders();
alert('Follow-up added successfully!');
}
// Update upcoming reminders
function updateUpcomingReminders() {
const listDiv = document.getElementById('upcomingRemindersList');
listDiv.innerHTML = '';
const today = new Date();
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
const upcomingReminders = reminders.filter(reminder => {
const reminderDate = new Date(reminder.date);
return !reminder.completed && reminderDate >= today && reminderDate <= nextWeek;
});
upcomingReminders.sort((a, b) => new Date(a.date) - new Date(b.date));
if (upcomingReminders.length === 0) {
listDiv.innerHTML = '<p>No upcoming reminders in the next 7 days.</p>';
return;
}
upcomingReminders.forEach(reminder => {
const reminderElement = document.createElement('div');
reminderElement.className = 'reminder-item';
const typeClass = `reminder-type ${reminder.type}`;
const typeText = reminder.type.charAt(0).toUpperCase() + reminder.type.slice(1);
reminderElement.innerHTML = `
<div class="reminder-header">
<span>${reminder.customerName}</span>
<span class="${typeClass}">${typeText}</span>
</div>
<p><strong>Date:</strong> ${new Date(reminder.date).toLocaleDateString()} ${new Date(reminder.date).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</p>
<p><strong>Notes:</strong> ${reminder.notes}</p>
<button onclick="completeReminder(${reminder.id})" class="success-button" style="margin-top: 10px;"><i class="fas fa-check"></i> <span>Mark Complete</span></button>
`;
listDiv.appendChild(reminderElement);
});
}
// Complete reminder
function completeReminder(reminderId) {
const reminderIndex = reminders.findIndex(r => r.id === reminderId);
if (reminderIndex !== -1) {
reminders[reminderIndex].completed = true;
localStorage.setItem('garageDoorReminders', JSON.stringify(reminders));
updateUpcomingReminders();
renderCalendar();
alert('Reminder marked as complete!');
}
}
// Data management functions
function exportData() {
const data = {
leads: leads,
jobs: jobs,
offers: offers,
customers: customers,
technicians: technicians,
strategies: strategies,
mistakes: mistakes,
partners: partners,
payments: payments,
followups: followups,
reminders: reminders,
techDetails: techDetails,
partnerExperiences: partnerExperiences,
customServices: customServices,
companies: companies,
offerCategories: offerCategories,
settings: settings
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('hidden', '');
a.setAttribute('href', url);
a.setAttribute('download', 'garage_door_business_data.json');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
alert('Data exported successfully!');
}
function importData() {
const fileInput = document.getElementById('data-file');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file to import');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
// Validate the data structure
if (data.leads && data.jobs && data.customers) {
// Import all data
leads = data.leads || [];
jobs = data.jobs || [];
offers = data.offers || [];
customers = data.customers || [];
technicians = data.technicians || [];
strategies = data.strategies || [];
mistakes = data.mistakes || [];
partners = data.partners || [];
payments = data.payments || [];
followups = data.followups || [];
reminders = data.reminders || [];
techDetails = data.techDetails || {};
partnerExperiences = data.partnerExperiences || {};
customServices = data.customServices || [];
companies = (data.companies && data.companies.length) ? data.companies : [{ id: 1, name: 'Murrieta Garage Doors', isDefault: true }];
offerCategories = data.offerCategories || [];
settings = mergeSettings(data.settings);
// Save to localStorage
localStorage.setItem('garageDoorLeads', JSON.stringify(leads));
localStorage.setItem('garageDoorJobs', JSON.stringify(jobs));
localStorage.setItem('garageDoorOffers', JSON.stringify(offers));
localStorage.setItem('garageDoorCustomers', JSON.stringify(customers));
localStorage.setItem('garageDoorTechnicians', JSON.stringify(technicians));
localStorage.setItem('garageDoorStrategies', JSON.stringify(strategies));
localStorage.setItem('garageDoorMistakes', JSON.stringify(mistakes));
localStorage.setItem('garageDoorPartners', JSON.stringify(partners));
localStorage.setItem('garageDoorPayments', JSON.stringify(payments));
localStorage.setItem('garageDoorFollowups', JSON.stringify(followups));
localStorage.setItem('garageDoorReminders', JSON.stringify(reminders));
localStorage.setItem('garageDoorTechDetails', JSON.stringify(techDetails));
localStorage.setItem('garageDoorPartnerExperiences', JSON.stringify(partnerExperiences));
localStorage.setItem('garageDoorCustomServices', JSON.stringify(customServices));
localStorage.setItem('garageDoorCompanies', JSON.stringify(companies));
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
document.getElementById('import-result').textContent = 'Data imported successfully!';
document.getElementById('import-result').style.color = 'var(--success-color)';
// Refresh all displays
updateLeadDropdown();
updateTechnicianDropdown();
updatePartnerDropdown();
updateCompletedJobsList();
updatePerformanceStats();
updateCustomerList();
updateCustomerDropdown();
updateAllOffersList();
updateTechniciansList();
updateStrategiesList();
updateMistakesList();
updatePartnersList();
renderCalendar();
updateUpcomingReminders();
updateMyAccount();
updateCharts();
initializeSettings();
updateCompanyDropdown();
generateOfferDropdown();
updateOfferCategoriesDropdown();
updateOfferCategoriesList();
updateCustomServiceList();
applyFieldVisibility();
applyJobFieldVisibility();
applyTabVisibility();
alert('Data imported successfully! All displays have been updated.');
} else {
throw new Error('Invalid data format');
}
} catch (error) {
document.getElementById('import-result').textContent = 'Error importing data: ' + error.message;
document.getElementById('import-result').style.color = 'var(--accent-color)';
}
};
reader.readAsText(file);
}
function resetData() {
if (confirm('Are you sure you want to reset ALL data? This action cannot be undone and will delete all your leads, jobs, customers, and settings.')) {
// Clear all data
leads = [];
jobs = [];
offers = [];
customers = [];
technicians = [];
strategies = [];
mistakes = [];
partners = [];
payments = [];
followups = [];
reminders = [];
techDetails = {};
partnerExperiences = {};
customServices = [];
companies = [{ id: 1, name: 'Murrieta Garage Doors', isDefault: true }];
offerCategories = [
{
id: 1,
name: 'Maintenance Services',
items: [
'Spring Replacement Package',
'Annual Lubrication Service',
'Full System Tune-Up'
]
},
{
id: 2,
name: 'Upgrade Services',
items: [
'Epoxy Floor Coating',
'New Garage Door Installation',
'Smart Opener Upgrade'
]
},
{
id: 3,
name: 'Custom Services',
items: [
'Garage Gym Installation',
'Custom Storage Solutions',
'Custom Design Package'
]
}
];
settings = {
theme: 'default',
buttonLayout: 'both',
buttonSize: 'medium',
tabLayout: 'horizontal',
titleDisplay: 'show',
tabButtonStyle: 'classic',
tabVisibility: {},
fieldVisibility: {},
jobFieldVisibility: {},
leadEntry: {
defaultTimeWindow: 'asap',
defaultServiceOption: 'free-estimate'
},
partnerSettings: {
defaultPartner: '',
defaultPartnerCommission: 50
},
reminders: {
enabled: false,
callReminder: false,
emailReminder: false,
maintenanceReminder: false,
reviewReminder: false
}
};
// Clear localStorage
localStorage.removeItem('garageDoorLeads');
localStorage.removeItem('garageDoorJobs');
localStorage.removeItem('garageDoorOffers');
localStorage.removeItem('garageDoorCustomers');
localStorage.removeItem('garageDoorTechnicians');
localStorage.removeItem('garageDoorStrategies');
localStorage.removeItem('garageDoorMistakes');
localStorage.removeItem('garageDoorPartners');
localStorage.removeItem('garageDoorPayments');
localStorage.removeItem('garageDoorFollowups');
localStorage.removeItem('garageDoorReminders');
localStorage.removeItem('garageDoorTechDetails');
localStorage.removeItem('garageDoorPartnerExperiences');
localStorage.removeItem('garageDoorCustomServices');
localStorage.removeItem('garageDoorCompanies');
localStorage.removeItem('garageDoorOfferCategories');
localStorage.removeItem('garageDoorSettings');
// Refresh all displays
updateLeadDropdown();
updateTechnicianDropdown();
updatePartnerDropdown();
updateCompletedJobsList();
updatePerformanceStats();
updateCustomerList();
updateCustomerDropdown();
updateAllOffersList();
updateTechniciansList();
updateStrategiesList();
updateMistakesList();
updatePartnersList();
renderCalendar();
updateUpcomingReminders();
updateMyAccount();
updateCharts();
initializeSettings();
alert('All data has been reset successfully!');
}
}
// Partner management functions
function addPartner() {
const name = document.getElementById('partner-name-input').value.trim();
const phone = document.getElementById('partner-phone').value.trim();
const email = document.getElementById('partner-email').value.trim();
const commission = parseInt(document.getElementById('partner-commission-input').value) || 50;
const status = document.getElementById('partner-status').value;
if (!name) {
alert('Please enter a partner name');
return;
}
if (partners.some(p => p.name === name)) {
alert('Partner already exists');
return;
}
partners.push({
id: Date.now(),
name: name,
phone: phone,
email: email,
commissionRate: commission,
status: status,
totalJobs: 0,
totalCommission: 0,
createdAt: new Date().toISOString()
});
localStorage.setItem('garageDoorPartners', JSON.stringify(partners));
// Clear form
document.getElementById('partner-name-input').value = '';
document.getElementById('partner-phone').value = '';
document.getElementById('partner-email').value = '';
document.getElementById('partner-commission-input').value = '50';
document.getElementById('partner-status').value = 'Active';
updatePartnersList();
updatePartnerDropdown();
alert('Partner added successfully!');
}
function updatePartner() {
// This would update an existing partner
// For now, we'll just use the add functionality
addPartner();
}
function updatePartnersList() {
const listDiv = document.getElementById('partnersList');
listDiv.innerHTML = '';
if (partners.length === 0) {
listDiv.innerHTML = '<p>No partners added yet.</p>';
return;
}
partners.forEach(partner => {
const partnerCard = document.createElement('div');
partnerCard.className = 'partner-card-item';
partnerCard.onclick = () => showPartnerDetails(partner.name);
partnerCard.innerHTML = `
<h4>${partner.name}</h4>
<p><strong>Phone:</strong> ${partner.phone || 'N/A'}</p>
<p><strong>Email:</strong> ${partner.email || 'N/A'}</p>
<p><strong>Commission Rate:</strong> ${partner.commissionRate}%</p>
<p><strong>Status:</strong> ${partner.status}</p>
<p><strong>Total Jobs:</strong> ${partner.totalJobs || 0}</p>
<p><strong>Total Commission:</strong> $${(partner.totalCommission || 0).toFixed(2)}</p>
`;
listDiv.appendChild(partnerCard);
});
}
function showPartnerDetails(partnerName) {
currentPartnerName = partnerName;
const partner = partners.find(p => p.name === partnerName);
if (!partner) return;
document.getElementById('partnerDetails').style.display = 'block';
// Update partner detail content
let detailContent = `
<div class="tech-stats">
<div class="tech-stat">
<div class="tech-stat-value">${partner.totalJobs || 0}</div>
<div class="tech-stat-label">Total Jobs</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">$${(partner.totalCommission || 0).toFixed(0)}</div>
<div class="tech-stat-label">Total Commission</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">${partner.commissionRate}%</div>
<div class="tech-stat-label">Commission Rate</div>
</div>
<div class="tech-stat">
<div class="tech-stat-value">${partner.status}</div>
<div class="tech-stat-label">Status</div>
</div>
</div>
`;
document.getElementById('partnerDetailContent').innerHTML = detailContent;
// Update partner experience content
updatePartnerExperienceContent(partnerName);
// Update job history
updatePartnerJobHistory(partnerName);
}
function updatePartnerExperienceContent(partnerName) {
const experiences = partnerExperiences[partnerName] || [];
let experienceContent = '';
if (experiences.length === 0) {
experienceContent = '<p>No work experiences recorded yet.</p>';
} else {
experiences.sort((a, b) => new Date(b.date) - new Date(a.date));
experiences.forEach(exp => {
const stars = '★'.repeat(exp.rating) + '☆'.repeat(5 - exp.rating);
const difficultyText =
exp.difficulty === 1 ? 'Very Easy' :
exp.difficulty === 2 ? 'Easy' :
exp.difficulty === 3 ? 'Moderate' :
exp.difficulty === 4 ? 'Difficult' : 'Very Difficult';
experienceContent += `
<div class="experience-item">
<div class="experience-actions">
<button onclick="editPartnerExperience(${exp.id})"><i class="fas fa-edit"></i></button>
<button class="danger-button" onclick="deletePartnerExperience(${exp.id})"><i class="fas fa-trash"></i></button>
</div>
<p><strong>Date:</strong> ${new Date(exp.date).toLocaleDateString()}</p>
<p><strong>Rating:</strong> ${stars} (${exp.rating}/5)</p>
<p><strong>Difficulty:</strong> ${difficultyText}</p>
<p><strong>Notes:</strong> ${exp.notes}</p>
</div>
`;
});
}
document.getElementById('partnerExperienceContent').innerHTML = experienceContent;
}
function addPartnerExperience() {
if (!currentPartnerName) {
alert('Please select a partner first');
return;
}
document.getElementById('partnerExperienceModal').style.display = 'block';
// Set default date to today
document.getElementById('experience-date').valueAsDate = new Date();
}
function closePartnerExperienceModal() {
document.getElementById('partnerExperienceModal').style.display = 'none';
}
function savePartnerExperience() {
if (!currentPartnerName) return;
const date = document.getElementById('experience-date').value;
const rating = parseInt(document.getElementById('experience-rating').value);
const difficulty = parseInt(document.getElementById('experience-difficulty').value);
const notes = document.getElementById('experience-notes').value;
if (!date) {
alert('Please select a date');
return;
}
const experience = {
id: Date.now(),
date: date,
rating: rating,
difficulty: difficulty,
notes: notes
};
if (!partnerExperiences[currentPartnerName]) {
partnerExperiences[currentPartnerName] = [];
}
partnerExperiences[currentPartnerName].push(experience);
localStorage.setItem('garageDoorPartnerExperiences', JSON.stringify(partnerExperiences));
closePartnerExperienceModal();
updatePartnerExperienceContent(currentPartnerName);
alert('Work experience added successfully!');
}
function editPartnerExperience(experienceId) {
// Implementation for editing partner experience
alert('Edit partner experience functionality would be implemented here');
}
function deletePartnerExperience(experienceId) {
if (confirm('Are you sure you want to delete this work experience?')) {
if (partnerExperiences[currentPartnerName]) {
const expIndex = partnerExperiences[currentPartnerName].findIndex(exp => exp.id === experienceId);
if (expIndex !== -1) {
partnerExperiences[currentPartnerName].splice(expIndex, 1);
localStorage.setItem('garageDoorPartnerExperiences', JSON.stringify(partnerExperiences));
updatePartnerExperienceContent(currentPartnerName);
alert('Work experience deleted successfully!');
}
}
}
}
function updatePartnerJobHistory(partnerName) {
const partnerJobs = jobs.filter(job => job.partner === partnerName);
let jobHistoryContent = '';
if (partnerJobs.length === 0) {
jobHistoryContent = '<p>No job history for this partner.</p>';
} else {
partnerJobs.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
partnerJobs.forEach(job => {
const lead = leads.find(l => l.id === job.leadId);
if (!lead) return;
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = netAfterTechnician * ((job.partnerCommissionRate || 0) / 100);
jobHistoryContent += `
<div class="job-item">
<div class="job-header">
<span>${new Date(job.completedAt).toLocaleDateString()}</span>
<span>$${partnerCommission.toFixed(2)}</span>
</div>
<p><strong>Customer:</strong> ${lead.name}</p>
<p><strong>Service:</strong> ${lead.notes}</p>
<p><strong>Commission Rate:</strong> ${job.partnerCommissionRate}%</p>
<p><strong>Partner Commission:</strong> $${partnerCommission.toFixed(2)}</p>
</div>
`;
});
}
document.getElementById('partnerJobHistoryContent').innerHTML = jobHistoryContent;
}
function editPartner() {
alert('Edit partner functionality would be implemented here');
}
function deletePartner() {
if (!currentPartnerName) return;
if (confirm('Are you sure you want to delete this partner? This will remove all their data.')) {
const partnerIndex = partners.findIndex(p => p.name === currentPartnerName);
if (partnerIndex !== -1) {
partners.splice(partnerIndex, 1);
localStorage.setItem('garageDoorPartners', JSON.stringify(partners));
// Also remove from partner experiences
delete partnerExperiences[currentPartnerName];
localStorage.setItem('garageDoorPartnerExperiences', JSON.stringify(partnerExperiences));
alert('Partner deleted successfully!');
updatePartnersList();
updatePartnerDropdown();
document.getElementById('partnerDetails').style.display = 'none';
}
}
}
// Settings functions
function initializeSettings() {
// Apply current settings
applyTheme(settings.theme);
applyButtonLayout(settings.buttonLayout);
applyButtonSize(settings.buttonSize);
applyTabLayout(settings.tabLayout);
applyTitleDisplay(settings.titleDisplay);
applyTabButtonStyle(settings.tabButtonStyle);
// Initialize reminder settings
if (settings.reminders) {
document.getElementById('enable-reminders').checked = settings.reminders.enabled;
document.getElementById('enable-call-reminder').checked = settings.reminders.callReminder;
document.getElementById('enable-email-reminder').checked = settings.reminders.emailReminder;
document.getElementById('enable-maintenance-reminder').checked = settings.reminders.maintenanceReminder;
document.getElementById('enable-review-reminder').checked = settings.reminders.reviewReminder;
toggleReminders();
}
// Initialize company dropdown
updateCompanyDropdown();
updateCompanyList();
// Initialize lead entry settings
if (settings.leadEntry) {
document.getElementById('default-time-window').value = settings.leadEntry.defaultTimeWindow;
document.getElementById('default-service-option').value = settings.leadEntry.defaultServiceOption;
}
// Initialize partner settings
if (settings.partnerSettings) {
document.getElementById('default-partner-commission').value = settings.partnerSettings.defaultPartnerCommission;
}
// Initialize job field controls
initializeJobFieldControls();
// Initialize tab visibility
initializeTabVisibility();
// Initialize field visibility
initializeFieldVisibility();
// Initialize offer categories
updateOfferCategoriesList();
}
// Theme application
function applyTheme(themeName) {
// Remove all theme classes
document.body.classList.remove('theme-dark', 'theme-green', 'theme-purple', 'theme-ocean');
// Add the selected theme class
if (themeName !== 'default') {
document.body.classList.add(`theme-${themeName}`);
}
// Update active state in theme options
document.querySelectorAll('.theme-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.theme-option[data-theme="${themeName}"]`);
if (_sel) _sel.classList.add('active');
settings.theme = themeName;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
// Button layout application
function applyButtonLayout(layout) {
// Remove all button layout classes
document.body.classList.remove('button-layout-icon', 'button-layout-text');
// Add the selected layout class
if (layout !== 'both') {
document.body.classList.add(`button-layout-${layout}`);
}
// Update active state in layout options
document.querySelectorAll('.layout-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.layout-option[data-layout="${layout}"]`);
if (_sel) _sel.classList.add('active');
settings.buttonLayout = layout;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
// Button size application
function applyButtonSize(size) {
// Remove all button size classes
document.body.classList.remove('button-size-small', 'button-size-large');
// Add the selected size class
if (size !== 'medium') {
document.body.classList.add(`button-size-${size}`);
}
// Update active state in size options
document.querySelectorAll('.size-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.size-option[data-size="${size}"]`);
if (_sel) _sel.classList.add('active');
settings.buttonSize = size;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
// Tab layout application
function applyTabLayout(layout) {
const tabs = document.getElementById('mainTabs');
// Remove all tab layout classes
tabs.classList.remove('horizontal', 'vertical', 'grid', 'compact');
// Add the selected layout class
tabs.classList.add(layout);
// Update active state in layout options
document.querySelectorAll('.tab-layout-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.tab-layout-option[data-layout="${layout}"]`);
if (_sel) _sel.classList.add('active');
settings.tabLayout = layout;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
// Title display application
function applyTitleDisplay(display) {
const title = document.getElementById('main-title');
if (display === 'hide') {
title.style.display = 'none';
} else {
title.style.display = 'block';
}
// Update active state in display options
document.querySelectorAll('.title-display-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.title-display-option[data-display="${display}"]`);
if (_sel) _sel.classList.add('active');
settings.titleDisplay = display;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
// Top bar button style application
const TAB_BUTTON_STYLES = ['classic', 'pills', 'underline', 'segmented', 'outlined', 'elevated', 'minimal'];
function applyTabButtonStyle(styleName) {
const style = TAB_BUTTON_STYLES.indexOf(styleName) !== -1 ? styleName : 'classic';
// Remove every style class, then apply the chosen one
TAB_BUTTON_STYLES.forEach(s => document.body.classList.remove(`tabstyle-${s}`));
document.body.classList.add(`tabstyle-${style}`);
// Update active state in the picker
document.querySelectorAll('.tab-style-option').forEach(option => {
option.classList.remove('active');
});
const _sel = document.querySelector(`.tab-style-option[data-tab-style="${style}"]`);
if (_sel) _sel.classList.add('active');
settings.tabButtonStyle = style;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
}
function saveTabButtonStyleSetting() {
// Already applied when selected, just confirm
alert('Top bar button style saved successfully!');
}
// Toggle reminders
function toggleReminders() {
const enabled = document.getElementById('enable-reminders').checked;
const options = document.getElementById('reminder-options');
if (enabled) {
options.style.display = 'block';
} else {
options.style.display = 'none';
}
}
// Save reminder settings
function saveReminderSettings() {
if (!settings.reminders) {
settings.reminders = {};
}
settings.reminders.enabled = document.getElementById('enable-reminders').checked;
settings.reminders.callReminder = document.getElementById('enable-call-reminder').checked;
settings.reminders.emailReminder = document.getElementById('enable-email-reminder').checked;
settings.reminders.maintenanceReminder = document.getElementById('enable-maintenance-reminder').checked;
settings.reminders.reviewReminder = document.getElementById('enable-review-reminder').checked;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
alert('Reminder settings saved successfully!');
}
// Save title display setting
function saveTitleDisplaySetting() {
// Already applied when selected, just show confirmation
alert('Title display setting saved successfully!');
}
// Save tab layout setting
function saveTabLayoutSetting() {
// Already applied when selected, just show confirmation
alert('Tab layout setting saved successfully!');
}
// Company management
function addCompany() {
const name = document.getElementById('new-company-name').value.trim();
if (!name) {
alert('Please enter a company name');
return;
}
if (companies.some(c => c.name === name)) {
alert('Company already exists');
return;
}
companies.push({
id: Date.now(),
name: name,
isDefault: false
});
localStorage.setItem('garageDoorCompanies', JSON.stringify(companies));
document.getElementById('new-company-name').value = '';
updateCompanyDropdown();
updateCompanyList();
alert('Company added successfully!');
}
function updateCompanyDropdown() {
const select = document.getElementById('company');
const defaultSelect = document.getElementById('default-company-select');
select.innerHTML = '';
if (defaultSelect) {
defaultSelect.innerHTML = '';
}
companies.forEach(company => {
const option = document.createElement('option');
option.value = company.name;
option.textContent = company.name;
if (company.isDefault) {
option.selected = true;
}
select.appendChild(option);
if (defaultSelect) {
const defaultOption = option.cloneNode(true);
defaultSelect.appendChild(defaultOption);
}
});
}
function updateCompanyList() {
const listDiv = document.getElementById('companyListContainer');
listDiv.innerHTML = '';
companies.forEach(company => {
const companyItem = document.createElement('div');
companyItem.className = 'company-list-item';
companyItem.innerHTML = `
<span>${company.name} ${company.isDefault ? '<span class="company-default-badge">Default</span>' : ''}</span>
<div class="company-list-actions">
${!company.isDefault ? `<button onclick="setDefaultCompany(${company.id})"><i class="fas fa-star"></i></button>` : ''}
<button class="danger-button" onclick="deleteCompany(${company.id})"><i class="fas fa-trash"></i></button>
</div>
`;
listDiv.appendChild(companyItem);
});
}
function setDefaultCompany(companyId) {
companies.forEach(company => {
company.isDefault = company.id === companyId;
});
localStorage.setItem('garageDoorCompanies', JSON.stringify(companies));
updateCompanyDropdown();
updateCompanyList();
alert('Default company updated successfully!');
}
function deleteCompany(companyId) {
if (confirm('Are you sure you want to delete this company?')) {
const companyIndex = companies.findIndex(c => c.id === companyId);
if (companyIndex !== -1) {
const company = companies[companyIndex];
if (company.isDefault) {
alert('Cannot delete the default company. Please set another company as default first.');
return;
}
companies.splice(companyIndex, 1);
localStorage.setItem('garageDoorCompanies', JSON.stringify(companies));
updateCompanyDropdown();
updateCompanyList();
alert('Company deleted successfully!');
}
}
}
function saveCompanySettings() {
// Settings are already saved when actions are performed
alert('Company settings saved successfully!');
}
function resetCompanySettings() {
if (confirm('Are you sure you want to reset company settings to default?')) {
companies = [{ id: 1, name: 'Murrieta Garage Doors', isDefault: true }];
localStorage.setItem('garageDoorCompanies', JSON.stringify(companies));
updateCompanyDropdown();
updateCompanyList();
alert('Company settings reset successfully!');
}
}
// Lead entry settings
function saveLeadEntrySettings() {
if (!settings.leadEntry) {
settings.leadEntry = {};
}
settings.leadEntry.defaultTimeWindow = document.getElementById('default-time-window').value;
settings.leadEntry.defaultServiceOption = document.getElementById('default-service-option').value;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
alert('Lead entry settings saved successfully!');
}
function resetLeadEntrySettings() {
document.getElementById('default-time-window').value = 'asap';
document.getElementById('default-service-option').value = 'free-estimate';
alert('Lead entry settings reset to default!');
}
// Job field controls
function initializeJobFieldControls() {
const grid = document.getElementById('job-field-controls-grid');
grid.innerHTML = '';
// Define job fields with labels
const jobFields = [
{ id: 'select-lead', label: 'Select Lead' },
{ id: 'lead-status-job', label: 'Lead Status' },
{ id: 'technician-name', label: 'Technician' },
{ id: 'payment-type', label: 'Payment Type' },
{ id: 'technician-commission', label: 'Technician Commission' },
{ id: 'flat-rate-amount', label: 'Flat Rate Amount' },
{ id: 'partner-section', label: 'Partner Section' },
{ id: 'job-income', label: 'Total Income' },
{ id: 'payment-method', label: 'Payment Method' },
{ id: 'parts-cost', label: 'Parts Cost' },
{ id: 'parts-list', label: 'Parts Used' },
{ id: 'job-duration', label: 'Job Duration' },
{ id: 'sale-strategy', label: 'Sales Strategy' },
{ id: 'job-difficulty', label: 'Job Difficulty' },
{ id: 'customer-satisfaction', label: 'Customer Satisfaction' },
{ id: 'left-review', label: 'Left Review' }
];
jobFields.forEach(field => {
const isVisible = !settings.jobFieldVisibility || settings.jobFieldVisibility[field.id] !== false;
const controlItem = document.createElement('div');
controlItem.className = 'job-field-control-item';
controlItem.innerHTML = `
<input type="checkbox" id="job-field-${field.id}" ${isVisible ? 'checked' : ''}>
<label for="job-field-${field.id}">${field.label}</label>
`;
grid.appendChild(controlItem);
});
}
function saveJobFieldSettings() {
if (!settings.jobFieldVisibility) {
settings.jobFieldVisibility = {};
}
// Get all job field checkboxes
const checkboxes = document.querySelectorAll('#job-field-controls-grid input[type="checkbox"]');
checkboxes.forEach(checkbox => {
const fieldId = checkbox.id.replace('job-field-', '');
settings.jobFieldVisibility[fieldId] = checkbox.checked;
});
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
// Apply visibility settings
applyJobFieldVisibility();
alert('Job field settings saved successfully!');
}
function applyJobFieldVisibility() {
if (!settings.jobFieldVisibility) return;
Object.keys(settings.jobFieldVisibility).forEach(fieldId => {
const isVisible = settings.jobFieldVisibility[fieldId];
const fieldElement = document.querySelector(`[data-job-field="${fieldId}"]`);
if (fieldElement) {
fieldElement.style.display = isVisible ? 'block' : 'none';
}
});
}
function resetJobFieldSettings() {
if (confirm('Are you sure you want to reset all job field settings to default?')) {
settings.jobFieldVisibility = {};
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
initializeJobFieldControls();
applyJobFieldVisibility();
alert('Job field settings reset successfully!');
}
}
// Partner settings
function savePartnerSettings() {
if (!settings.partnerSettings) {
settings.partnerSettings = {};
}
settings.partnerSettings.defaultPartner = document.getElementById('default-partner').value;
settings.partnerSettings.defaultPartnerCommission = parseInt(document.getElementById('default-partner-commission').value) || 50;
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
alert('Partner settings saved successfully!');
}
function resetPartnerSettings() {
document.getElementById('default-partner').value = '';
document.getElementById('default-partner-commission').value = '50';
alert('Partner settings reset to default!');
}
// Tab visibility
function initializeTabVisibility() {
const list = document.getElementById('tab-visibility-list');
list.innerHTML = '';
// Define tabs with labels and default visibility
const tabs = [
{ id: 'lead-entry', label: 'Lead Entry', visible: true },
{ id: 'job-details', label: 'Job Details', visible: true },
{ id: 'customer-management', label: 'Customer Management', visible: true },
{ id: 'offers', label: 'Future Offers', visible: true },
{ id: 'performance', label: 'Performance', visible: true },
{ id: 'projections', label: 'Projections', visible: true },
{ id: 'charts', label: 'Charts', visible: true },
{ id: 'technicians', label: 'Technicians', visible: true },
{ id: 'my-account', label: 'My Account', visible: true },
{ id: 'strategies', label: 'Sales Strategies', visible: true },
{ id: 'mistakes', label: 'Common Mistakes', visible: true },
{ id: 'calendar', label: 'Calendar', visible: true },
{ id: 'data-management', label: 'Data Tools', visible: true },
{ id: 'partner-management', label: 'Partner Management', visible: true },
{ id: 'settings', label: 'Settings', visible: true }
];
tabs.forEach(tab => {
const isVisible = !settings.tabVisibility || settings.tabVisibility[tab.id] !== false;
const tabItem = document.createElement('div');
tabItem.className = 'tab-visibility-item';
tabItem.innerHTML = `
<div>
<input type="checkbox" id="tab-${tab.id}" ${isVisible ? 'checked' : ''}>
<label for="tab-${tab.id}">${tab.label}</label>
</div>
<div class="tab-visibility-controls">
<div class="tab-order-buttons">
<button onclick="moveTabUp('${tab.id}')"><i class="fas fa-arrow-up"></i></button>
<button onclick="moveTabDown('${tab.id}')"><i class="fas fa-arrow-down"></i></button>
</div>
</div>
`;
list.appendChild(tabItem);
});
}
function moveTabUp(tabId) {
// Implementation for moving tab up in order
alert(`Move ${tabId} up - functionality would be implemented here`);
}
function moveTabDown(tabId) {
// Implementation for moving tab down in order
alert(`Move ${tabId} down - functionality would be implemented here`);
}
function saveTabSettings() {
if (!settings.tabVisibility) {
settings.tabVisibility = {};
}
// Get all tab checkboxes
const checkboxes = document.querySelectorAll('#tab-visibility-list input[type="checkbox"]');
checkboxes.forEach(checkbox => {
const tabId = checkbox.id.replace('tab-', '');
settings.tabVisibility[tabId] = checkbox.checked;
});
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
// Apply tab visibility
applyTabVisibility();
alert('Tab settings saved successfully!');
}
function applyTabVisibility() {
if (!settings.tabVisibility) return;
Object.keys(settings.tabVisibility).forEach(tabId => {
const isVisible = settings.tabVisibility[tabId];
const tabElement = document.querySelector(`.tab[data-tab="${tabId}"]`);
if (tabElement) {
tabElement.style.display = isVisible ? 'flex' : 'none';
}
});
}
function resetTabSettings() {
if (confirm('Are you sure you want to reset all tab settings to default?')) {
settings.tabVisibility = {};
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
initializeTabVisibility();
applyTabVisibility();
alert('Tab settings reset successfully!');
}
}
// Field visibility
function initializeFieldVisibility() {
const list = document.getElementById('field-visibility-list');
list.innerHTML = '';
// Define lead entry fields with labels
const fields = [
{ id: 'customer-name', label: 'Customer Name' },
{ id: 'customer-address', label: 'Address' },
{ id: 'customer-phone', label: 'Phone Number' },
{ id: 'customer-email', label: 'Email' },
{ id: 'company', label: 'Company' },
{ id: 'service-date', label: 'Date' },
{ id: 'service-time', label: 'Time Window' },
{ id: 'service-notes', label: 'Service Needed' },
{ id: 'service-pricing', label: 'Pricing Notes' },
{ id: 'marketing-source', label: 'Marketing Source' }
];
fields.forEach(field => {
const isVisible = !settings.fieldVisibility || settings.fieldVisibility[field.id] !== false;
const fieldItem = document.createElement('div');
fieldItem.className = 'field-visibility-item';
fieldItem.innerHTML = `
<div>
<input type="checkbox" id="field-${field.id}" ${isVisible ? 'checked' : ''}>
<label for="field-${field.id}">${field.label}</label>
</div>
`;
list.appendChild(fieldItem);
});
}
function saveFieldSettings() {
if (!settings.fieldVisibility) {
settings.fieldVisibility = {};
}
// Get all field checkboxes
const checkboxes = document.querySelectorAll('#field-visibility-list input[type="checkbox"]');
checkboxes.forEach(checkbox => {
const fieldId = checkbox.id.replace('field-', '');
settings.fieldVisibility[fieldId] = checkbox.checked;
});
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
// Apply field visibility
applyFieldVisibility();
alert('Field settings saved successfully!');
}
function applyFieldVisibility() {
if (!settings.fieldVisibility) return;
Object.keys(settings.fieldVisibility).forEach(fieldId => {
const isVisible = settings.fieldVisibility[fieldId];
const fieldElement = document.querySelector(`[data-field="${fieldId}"]`);
if (fieldElement) {
fieldElement.style.display = isVisible ? 'block' : 'none';
}
});
}
function resetFieldSettings() {
if (confirm('Are you sure you want to reset all field settings to default?')) {
settings.fieldVisibility = {};
localStorage.setItem('garageDoorSettings', JSON.stringify(settings));
initializeFieldVisibility();
applyFieldVisibility();
alert('Field settings reset successfully!');
}
}
// Offer management
function addOfferCategory() {
const name = document.getElementById('new-offer-category').value.trim();
if (!name) {
alert('Please enter a category name');
return;
}
if (offerCategories.some(c => c.name === name)) {
alert('Category already exists');
return;
}
offerCategories.push({
id: Date.now(),
name: name,
items: []
});
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
document.getElementById('new-offer-category').value = '';
updateOfferCategoriesList();
updateOfferCategoriesDropdown();
alert('Offer category added successfully!');
}
function addOfferItem() {
const name = document.getElementById('new-offer-item').value.trim();
const categoryId = parseInt(document.getElementById('offer-category-select').value);
if (!name) {
alert('Please enter an offer item name');
return;
}
if (!categoryId) {
alert('Please select a category');
return;
}
const category = offerCategories.find(c => c.id === categoryId);
if (!category) return;
if (category.items.some(item => item === name)) {
alert('Offer item already exists in this category');
return;
}
category.items.push(name);
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
document.getElementById('new-offer-item').value = '';
updateOfferCategoriesList();
generateOfferDropdown();
alert('Offer item added successfully!');
}
function updateOfferCategoriesDropdown() {
const select = document.getElementById('offer-category-select');
select.innerHTML = '<option value="">Select category</option>';
offerCategories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.name;
select.appendChild(option);
});
}
function updateOfferCategoriesList() {
const listDiv = document.getElementById('offer-categories-list');
listDiv.innerHTML = '';
offerCategories.forEach(category => {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'offer-group';
categoryDiv.innerHTML = `
<h4>${category.name}</h4>
<div class="offer-list-actions">
<button onclick="deleteOfferCategory(${category.id})" class="danger-button"><i class="fas fa-trash"></i> <span>Delete Category</span></button>
</div>
`;
if (category.items.length > 0) {
category.items.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.className = 'offer-item';
itemDiv.innerHTML = `
<span>${item}</span>
<button onclick="deleteOfferItem(${category.id}, '${escAttr(item)}')" class="danger-button" style="padding: 4px 8px; font-size: 11px;"><i class="fas fa-times"></i></button>
`;
categoryDiv.appendChild(itemDiv);
});
} else {
const emptyMsg = document.createElement('p');
emptyMsg.textContent = 'No items in this category yet.';
emptyMsg.style.fontStyle = 'italic';
emptyMsg.style.color = '#666';
categoryDiv.appendChild(emptyMsg);
}
listDiv.appendChild(categoryDiv);
});
}
function deleteOfferCategory(categoryId) {
if (confirm('Are you sure you want to delete this offer category? All items in this category will also be deleted.')) {
const categoryIndex = offerCategories.findIndex(c => c.id === categoryId);
if (categoryIndex !== -1) {
offerCategories.splice(categoryIndex, 1);
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
updateOfferCategoriesList();
updateOfferCategoriesDropdown();
generateOfferDropdown();
alert('Offer category deleted successfully!');
}
}
}
function deleteOfferItem(categoryId, itemName) {
if (confirm('Are you sure you want to delete this offer item?')) {
const category = offerCategories.find(c => c.id === categoryId);
if (category) {
const itemIndex = category.items.findIndex(item => item === itemName);
if (itemIndex !== -1) {
category.items.splice(itemIndex, 1);
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
updateOfferCategoriesList();
generateOfferDropdown();
alert('Offer item deleted successfully!');
}
}
}
}
function saveOfferSettings() {
// Settings are already saved when actions are performed
alert('Offer settings saved successfully!');
}
function resetOfferSettings() {
if (confirm('Are you sure you want to reset all offer settings to default? This will delete all custom categories and items.')) {
offerCategories = [
{
id: 1,
name: 'Maintenance Services',
items: [
'Spring Replacement Package',
'Annual Lubrication Service',
'Full System Tune-Up'
]
},
{
id: 2,
name: 'Upgrade Services',
items: [
'Epoxy Floor Coating',
'New Garage Door Installation',
'Smart Opener Upgrade'
]
},
{
id: 3,
name: 'Custom Services',
items: [
'Garage Gym Installation',
'Custom Storage Solutions',
'Custom Design Package'
]
}
];
localStorage.setItem('garageDoorOfferCategories', JSON.stringify(offerCategories));
updateOfferCategoriesList();
updateOfferCategoriesDropdown();
generateOfferDropdown();
alert('Offer settings reset to default!');
}
}
// Service options functions
function selectServiceOption(option) {
const options = document.querySelectorAll('.service-option');
options.forEach(opt => opt.classList.remove('active'));
const _opt = window.event && (window.event.currentTarget || window.event.target.closest('.service-option'));
if (_opt && _opt.classList) _opt.classList.add('active');
const customInput = document.getElementById('custom-service-input');
const serviceList = document.getElementById('service-list-container');
if (option === 'custom') {
customInput.classList.add('show');
serviceList.style.display = 'block';
updateCustomServiceList();
} else {
customInput.classList.remove('show');
serviceList.style.display = 'none';
}
}
function addCustomService() {
const serviceInput = document.getElementById('new-service-input');
const serviceName = serviceInput.value.trim();
if (!serviceName) {
alert('Please enter a service name');
return;
}
if (customServices.includes(serviceName)) {
alert('Service already exists');
return;
}
customServices.push(serviceName);
localStorage.setItem('garageDoorCustomServices', JSON.stringify(customServices));
serviceInput.value = '';
updateCustomServiceList();
alert('Custom service added successfully!');
}
function updateCustomServiceList() {
const listDiv = document.getElementById('service-list-container');
listDiv.innerHTML = '';
if (customServices.length === 0) {
listDiv.innerHTML = '<p>No custom services added yet.</p>';
return;
}
customServices.forEach(service => {
const serviceItem = document.createElement('div');
serviceItem.className = 'service-list-item';
serviceItem.innerHTML = `
<span>${service}</span>
<div class="service-list-actions">
<button onclick="useCustomService('${escAttr(service)}')"><i class="fas fa-check"></i></button>
<button class="danger-button" onclick="deleteCustomService('${escAttr(service)}')"><i class="fas fa-trash"></i></button>
</div>
`;
listDiv.appendChild(serviceItem);
});
}
function useCustomService(serviceName) {
document.getElementById('service-notes').value = serviceName;
selectServiceOption('free-estimate'); // Switch back to free estimate to hide custom inputs
}
function deleteCustomService(serviceName) {
if (confirm('Are you sure you want to delete this custom service?')) {
const serviceIndex = customServices.findIndex(s => s === serviceName);
if (serviceIndex !== -1) {
customServices.splice(serviceIndex, 1);
localStorage.setItem('garageDoorCustomServices', JSON.stringify(customServices));
updateCustomServiceList();
alert('Custom service deleted successfully!');
}
}
}
// Edit customer modal functions
function openEditCustomerModal(customer) {
document.getElementById('edit-customer-id').value = customer.id;
document.getElementById('edit-customer-name').value = customer.name;
document.getElementById('edit-customer-address').value = customer.address;
document.getElementById('edit-customer-phone').value = customer.phone;
document.getElementById('edit-customer-email').value = customer.email || '';
document.getElementById('edit-customer-house-value').value = customer.houseValue || '';
document.getElementById('edit-customer-garage-type').value = customer.garageType || '';
document.getElementById('edit-customer-garage-size').value = customer.garageSize || '';
document.getElementById('edit-customer-door-age').value = customer.doorAge || '';
document.getElementById('edit-customer-door-type').value = customer.doorType || '';
document.getElementById('edit-customer-opener-type').value = customer.openerType || '';
document.getElementById('edit-customer-notes').value = customer.notes || '';
document.getElementById('edit-customer-future-services').value = customer.futureServices || '';
document.getElementById('editCustomerModal').style.display = 'block';
}
function closeEditCustomerModal() {
document.getElementById('editCustomerModal').style.display = 'none';
}
function saveEditedCustomer() {
const customerId = parseInt(document.getElementById('edit-customer-id').value);
const customerIndex = customers.findIndex(c => c.id === customerId);
if (customerIndex === -1) {
alert('Customer not found');
return;
}
customers[customerIndex].name = document.getElementById('edit-customer-name').value;
customers[customerIndex].address = document.getElementById('edit-customer-address').value;
customers[customerIndex].phone = document.getElementById('edit-customer-phone').value;
customers[customerIndex].email = document.getElementById('edit-customer-email').value;
customers[customerIndex].houseValue = document.getElementById('edit-customer-house-value').value;
customers[customerIndex].garageType = document.getElementById('edit-customer-garage-type').value;
customers[customerIndex].garageSize = document.getElementById('edit-customer-garage-size').value;
customers[customerIndex].doorAge = document.getElementById('edit-customer-door-age').value;
customers[customerIndex].doorType = document.getElementById('edit-customer-door-type').value;
customers[customerIndex].openerType = document.getElementById('edit-customer-opener-type').value;
customers[customerIndex].notes = document.getElementById('edit-customer-notes').value;
customers[customerIndex].futureServices = document.getElementById('edit-customer-future-services').value;
localStorage.setItem('garageDoorCustomers', JSON.stringify(customers));
closeEditCustomerModal();
updateCustomerList();
// Refresh customer details if they're currently being viewed
if (document.getElementById('customerDetails').style.display === 'block') {
showCustomerDetails(customers[customerIndex]);
}
alert('Customer details updated successfully!');
}
// Partner payment management
function updatePartnerPaymentInfo(partner) {
const partnerJobs = jobs.filter(job => job.partner === partner.name);
let totalCommissionOwed = 0;
partnerJobs.forEach(job => {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = netAfterTechnician * ((job.partnerCommissionRate || 0) / 100);
// Check if payment exists for this job
const paymentExists = payments.some(p => p.jobId === job.id);
if (!paymentExists) {
totalCommissionOwed += partnerCommission;
}
});
document.getElementById('total-commission-owed').textContent = `$${totalCommissionOwed.toFixed(2)}`;
updatePartnerPaymentHistory(partner.name);
}
function addPartnerPayment() {
const partnerName = document.getElementById('partner-name').value;
const amount = parseFloat(document.getElementById('payment-amount').value) || 0;
const date = document.getElementById('payment-date').value;
const method = document.getElementById('payment-method-partner').value;
const notes = document.getElementById('payment-notes').value;
if (!partnerName) {
alert('Please select a partner first');
return;
}
if (amount <= 0) {
alert('Please enter a valid payment amount');
return;
}
if (!date) {
alert('Please select a payment date');
return;
}
// For simplicity, we'll just add a payment record
// In a real app, you'd want to associate this with specific jobs
const payment = {
id: Date.now(),
partnerName: partnerName,
amount: amount,
date: date,
method: method,
notes: notes,
createdAt: new Date().toISOString()
};
payments.push(payment);
localStorage.setItem('garageDoorPayments', JSON.stringify(payments));
// Clear form
document.getElementById('payment-amount').value = '';
document.getElementById('payment-date').value = '';
document.getElementById('payment-method-partner').value = 'Cash';
document.getElementById('payment-notes').value = '';
updatePartnerPaymentInfo(partners.find(p => p.name === partnerName));
alert('Payment recorded successfully!');
}
function markPaymentPaid() {
const partnerName = document.getElementById('partner-name').value;
if (!partnerName) {
alert('Please select a partner first');
return;
}
// This would mark all outstanding commissions as paid
// In a real app, you'd want more granular control
const partnerJobs = jobs.filter(job => job.partner === partnerName);
partnerJobs.forEach(job => {
const paymentExists = payments.some(p => p.jobId === job.id);
if (!paymentExists) {
const gross = (job.income || 0) - (job.partsCost || 0);
let techCommission;
if (job.paymentType === 'flat') {
techCommission = job.flatRateAmount || 0;
} else {
techCommission = gross * ((job.commissionRate || 0) / 100);
}
const netAfterTechnician = gross - techCommission;
const partnerCommission = netAfterTechnician * ((job.partnerCommissionRate || 0) / 100);
const payment = {
id: Date.now() + Math.floor(Math.random() * 100000), // FIX: was colliding in-loop
partnerName: partnerName,
jobId: job.id,
amount: partnerCommission,
date: new Date().toISOString().split('T')[0],
method: 'Cash',
notes: 'Marked as paid',
createdAt: new Date().toISOString()
};
payments.push(payment);
}
});
localStorage.setItem('garageDoorPayments', JSON.stringify(payments));
updatePartnerPaymentInfo(partners.find(p => p.name === partnerName));
alert('All outstanding commissions marked as paid!');
}
function updatePartnerPaymentHistory(partnerName) {
const partnerPayments = payments.filter(p => p.partnerName === partnerName);
let paymentHistoryContent = '';
if (partnerPayments.length === 0) {
paymentHistoryContent = '<p>No payment history for this partner.</p>';
} else {
partnerPayments.sort((a, b) => new Date(b.date) - new Date(a.date));
partnerPayments.forEach(payment => {
paymentHistoryContent += `
<div class="payment-item">
<div class="payment-header">
<span>${new Date(payment.date).toLocaleDateString()}</span>
<span>$${payment.amount.toFixed(2)}</span>
</div>
<p><strong>Method:</strong> ${payment.method}</p>
<p><strong>Notes:</strong> ${payment.notes || 'N/A'}</p>
</div>
`;
});
}
document.getElementById('paymentHistoryContent').innerHTML = paymentHistoryContent;
}
// Initialize the application
function initializeApp() {
// Set default date to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('service-date').value = today;
document.getElementById('followup-date').value = today;
document.getElementById('experience-date').value = today;
// Set default time to current time + 1 hour
const now = new Date();
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
const startTime = formatTimeForInput(now);
const endTime = formatTimeForInput(oneHourLater);
document.getElementById('start-time').value = startTime;
document.getElementById('end-time').value = endTime;
document.getElementById('followup-time').value = startTime;
// Update dropdowns
updateLeadDropdown();
updateTechnicianDropdown();
updatePartnerDropdown();
updateCustomerDropdown();
updateCompanyDropdown();
// Update displays
updateCompletedJobsList();
updatePerformanceStats();
updateCustomerList();
updateAllOffersList();
updateTechniciansList();
updateStrategiesList();
updateMistakesList();
updatePartnersList();
// Initialize charts
updateCharts();
// Initialize calendar
renderCalendar();
updateUpcomingReminders();
// Initialize settings
initializeSettings();
// Apply field visibility settings
applyFieldVisibility();
applyJobFieldVisibility();
applyTabVisibility();
// Generate offer dropdown
generateOfferDropdown();
updateOfferCategoriesDropdown();
updateOfferCategoriesList();
// Update custom service list
updateCustomServiceList();
console.log('Garage Door Lead Management System initialized successfully!');
}
// Helper function to format time for time input
function formatTimeForInput(date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
// Update lead generated count (for performance metrics)
function updateLeadGeneratedCount() {
// This would update a counter display if needed
// Currently handled by updatePerformanceStats()
}
// Initialize when the page loads
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
// Set up event listeners for settings that need immediate application
document.querySelectorAll('.theme-option').forEach(option => {
option.addEventListener('click', function() {
applyTheme(this.getAttribute('data-theme'));
});
});
document.querySelectorAll('.layout-option').forEach(option => {
option.addEventListener('click', function() {
applyButtonLayout(this.getAttribute('data-layout'));
});
});
document.querySelectorAll('.size-option').forEach(option => {
option.addEventListener('click', function() {
applyButtonSize(this.getAttribute('data-size'));
});
});
document.querySelectorAll('.tab-layout-option').forEach(option => {
option.addEventListener('click', function() {
applyTabLayout(this.getAttribute('data-layout'));
});
});
document.querySelectorAll('.title-display-option').forEach(option => {
option.addEventListener('click', function() {
applyTitleDisplay(this.getAttribute('data-display'));
});
});
document.querySelectorAll('.tab-style-option').forEach(option => {
option.addEventListener('click', function() {
applyTabButtonStyle(this.getAttribute('data-tab-style'));
});
});
});
// Close modals when clicking outside
window.onclick = function(event) {
const modals = document.getElementsByClassName('modal');
for (let i = 0; i < modals.length; i++) {
if (event.target === modals[i]) {
modals[i].style.display = 'none';
}
}
}
// --- FIX: modal handlers referenced by #techDetailModal markup were missing ---
function closeTechDetailModal() {
const m = document.getElementById('techDetailModal');
if (m) m.style.display = 'none';
}
function saveTechnicianDetail() {
// markup calls the singular name; delegate to the real implementation
saveTechnicianDetails();
closeTechDetailModal();
}
window.closeTechDetailModal = closeTechDetailModal;
window.saveTechnicianDetail = saveTechnicianDetail;
// Export functions for global access
window.openTab = openTab;
window.toggleCompactSection = toggleCompactSection;
window.toggleSettingsSection = toggleSettingsSection;
window.toggleSheetSection = toggleSheetSection;
window.togglePaymentType = togglePaymentType;
window.updateCommissionValue = updateCommissionValue;
window.updatePartnerCommissionValue = updatePartnerCommissionValue;
window.updatePartnerCommissionFromSelect = updatePartnerCommissionFromSelect;
window.filterLeads = filterLeads;
window.generateLead = generateLead;
window.setTimeWindow = setTimeWindow;
window.shareViaWhatsApp = shareViaWhatsApp;
window.copyToClipboard = copyToClipboard;
window.shareViaEmail = shareViaEmail;
window.addToGoogleCalendar = addToGoogleCalendar;
window.saveLead = saveLead;
window.clearForm = clearForm;
window.loadLeadDetails = loadLeadDetails;
window.saveJobDetails = saveJobDetails;
window.calculateProfit = calculateProfit;
window.shareProfitViaWhatsApp = shareProfitViaWhatsApp;
window.copyProfitToClipboard = copyProfitToClipboard;
window.shareProfitViaEmail = shareProfitViaEmail;
window.deleteJob = deleteJob;
window.openEditJobModal = openEditJobModal;
window.closeEditJobModal = closeEditJobModal;
window.saveEditedJob = saveEditedJob;
window.sortTable = sortTable;
window.filterJobsByDate = filterJobsByDate;
window.resetJobDateFilter = resetJobDateFilter;
window.calculateLeadCost = calculateLeadCost;
window.exportToPDF = exportToPDF;
window.exportToCSV = exportToCSV;
window.exportToGoogleSheets = exportToGoogleSheets;
window.calculateProjections = calculateProjections;
window.searchCustomers = searchCustomers;
window.showCustomerDetails = showCustomerDetails;
window.updateCustomerStatus = updateCustomerStatus;
window.prepareOffer = prepareOffer;
window.contactCustomer = contactCustomer;
window.editCustomer = editCustomer;
window.deleteCustomer = deleteCustomer;
window.loadCustomerForOffer = loadCustomerForOffer;
window.toggleOfferDropdown = toggleOfferDropdown;
window.generateOffer = generateOffer;
window.createNewOffer = createNewOffer;
window.shareOfferViaWhatsApp = shareOfferViaWhatsApp;
window.copyOfferToClipboard = copyOfferToClipboard;
window.shareOfferViaEmail = shareOfferViaEmail;
window.saveOffer = saveOffer;
window.openEditOfferModal = openEditOfferModal;
window.closeEditOfferModal = closeEditOfferModal;
window.saveEditedOffer = saveEditedOffer;
window.deleteOffer = deleteOffer;
window.addTechnician = addTechnician;
window.showTechnicianDetails = showTechnicianDetails;
window.saveTechnicianDetails = saveTechnicianDetails;
window.resetTechnicianDetails = resetTechnicianDetails;
window.deleteTechnicianDetails = deleteTechnicianDetails;
window.editTechnician = editTechnician;
window.deleteTechnician = deleteTechnician;
window.togglePaymentFilter = togglePaymentFilter;
window.markJobAsPaid = markJobAsPaid;
window.addStrategy = addStrategy;
window.saveStrategy = saveStrategy;
window.openEditStrategyModal = openEditStrategyModal;
window.closeEditStrategyModal = closeEditStrategyModal;
window.saveEditedStrategy = saveEditedStrategy;
window.deleteStrategy = deleteStrategy;
window.addMistake = addMistake;
window.saveMistake = saveMistake;
window.openEditMistakeModal = openEditMistakeModal;
window.closeEditMistakeModal = closeEditMistakeModal;
window.saveEditedMistake = saveEditedMistake;
window.deleteMistake = deleteMistake;
window.previousMonth = previousMonth;
window.currentMonth = currentMonth;
window.nextMonth = nextMonth;
window.dragLead = dragLead;
window.dropLead = dropLead;
window.showJobDetails = showJobDetails;
window.closeJobDetailsModal = closeJobDetailsModal;
window.addFollowUp = addFollowUp;
window.closeFollowUpModal = closeFollowUpModal;
window.saveFollowUp = saveFollowUp;
window.completeReminder = completeReminder;
window.exportData = exportData;
window.importData = importData;
window.resetData = resetData;
window.addPartner = addPartner;
window.updatePartner = updatePartner;
window.showPartnerDetails = showPartnerDetails;
window.addPartnerExperience = addPartnerExperience;
window.closePartnerExperienceModal = closePartnerExperienceModal;
window.savePartnerExperience = savePartnerExperience;
window.editPartnerExperience = editPartnerExperience;
window.deletePartnerExperience = deletePartnerExperience;
window.editPartner = editPartner;
window.deletePartner = deletePartner;
window.toggleReminders = toggleReminders;
window.saveReminderSettings = saveReminderSettings;
window.saveTitleDisplaySetting = saveTitleDisplaySetting;
window.applyTabButtonStyle = applyTabButtonStyle;
window.saveTabButtonStyleSetting = saveTabButtonStyleSetting;
window.saveTabLayoutSetting = saveTabLayoutSetting;
window.addCompany = addCompany;
window.setDefaultCompany = setDefaultCompany;
window.deleteCompany = deleteCompany;
window.saveCompanySettings = saveCompanySettings;
window.resetCompanySettings = resetCompanySettings;
window.saveLeadEntrySettings = saveLeadEntrySettings;
window.resetLeadEntrySettings = resetLeadEntrySettings;
window.saveJobFieldSettings = saveJobFieldSettings;
window.resetJobFieldSettings = resetJobFieldSettings;
window.savePartnerSettings = savePartnerSettings;
window.resetPartnerSettings = resetPartnerSettings;
window.moveTabUp = moveTabUp;
window.moveTabDown = moveTabDown;
window.saveTabSettings = saveTabSettings;
window.resetTabSettings = resetTabSettings;
window.saveFieldSettings = saveFieldSettings;
window.resetFieldSettings = resetFieldSettings;
window.addOfferCategory = addOfferCategory;
window.addOfferItem = addOfferItem;
window.deleteOfferCategory = deleteOfferCategory;
window.deleteOfferItem = deleteOfferItem;
window.saveOfferSettings = saveOfferSettings;
window.resetOfferSettings = resetOfferSettings;
window.selectServiceOption = selectServiceOption;
window.addCustomService = addCustomService;
window.useCustomService = useCustomService;
window.deleteCustomService = deleteCustomService;
window.openEditCustomerModal = openEditCustomerModal;
window.closeEditCustomerModal = closeEditCustomerModal;
window.saveEditedCustomer = saveEditedCustomer;
window.addPartnerPayment = addPartnerPayment;
window.markPaymentPaid = markPaymentPaid;
/* ===================================================================
FIREBASE CLOUD SYNC — appended patch block
Realtime Database + Google Sign-In. Mirrors every garageDoor* key
to leadManagement/<uid> and applies remote changes live.
=================================================================== */
const FB_SDK_BASE = 'https://www.gstatic.com/firebasejs/10.12.2/';
const FB_CFG_KEY = 'garageDoorFirebaseConfig';
const FB_STATE_KEY = 'garageDoorFirebaseState';
// Every key that participates in sync. Deliberately excludes the two
// keys above so the config never round-trips through the cloud.
const FB_DATA_KEYS = [
'garageDoorLeads', 'garageDoorJobs', 'garageDoorOffers', 'garageDoorCustomers',
'garageDoorTechnicians', 'garageDoorStrategies', 'garageDoorMistakes',
'garageDoorPartners', 'garageDoorPayments', 'garageDoorFollowups',
'garageDoorReminders', 'garageDoorTechDetails', 'garageDoorPartnerExperiences',
'garageDoorCustomServices', 'garageDoorCompanies', 'garageDoorOfferCategories',
'garageDoorSettings'
];
let fbApp = null, fbAuth = null, fbDb = null, fbUser = null;
let fbRef = null, fbConnRef = null;
let fbSuppress = false; // true while writing remote data locally
let fbPushTimer = null;
let fbOnline = false;
let fbLastPushAt = 0;
// Per-tab id so a second tab on the same machine still receives updates,
// while this tab ignores the echo of its own write.
const fbSessionId = 'S' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
let fbState = (function () {
try {
const s = JSON.parse(localStorage.getItem(FB_STATE_KEY));
if (s && typeof s === 'object') return s;
} catch (e) { /* fall through to defaults */ }
return { autoSync: true, autoConnect: false, lastSync: null, pending: false };
})();
function fbSaveState() {
try { localStorage.setItem(FB_STATE_KEY, JSON.stringify(fbState)); } catch (e) { /* quota */ }
}
// ---------- config ----------
function fbParseConfig(text) {
let t = String(text || '').trim();
if (!t) return null;
t = t.replace(/^\s*(?:const|let|var)\s+\w+\s*=\s*/, '').replace(/;\s*$/, '');
try { return JSON.parse(t); } catch (e) { /* not strict JSON, try literal */ }
try { return (new Function('return (' + t + ')'))(); } catch (e) { return null; }
}
function fbGetConfig() {
try { return JSON.parse(localStorage.getItem(FB_CFG_KEY)); } catch (e) { return null; }
}
// ---------- data bridge ----------
function fbSnapshot() {
return {
garageDoorLeads: leads,
garageDoorJobs: jobs,
garageDoorOffers: offers,
garageDoorCustomers: customers,
garageDoorTechnicians: technicians,
garageDoorStrategies: strategies,
garageDoorMistakes: mistakes,
garageDoorPartners: partners,
garageDoorPayments: payments,
garageDoorFollowups: followups,
garageDoorReminders: reminders,
garageDoorTechDetails: techDetails,
garageDoorPartnerExperiences: partnerExperiences,
garageDoorCustomServices: customServices,
garageDoorCompanies: companies,
garageDoorOfferCategories: offerCategories,
garageDoorSettings: settings
};
}
function fbAssign(key, value) {
switch (key) {
case 'garageDoorLeads': leads = value || []; break;
case 'garageDoorJobs': jobs = value || []; break;
case 'garageDoorOffers': offers = value || []; break;
case 'garageDoorCustomers': customers = value || []; break;
case 'garageDoorTechnicians': technicians = value || []; break;
case 'garageDoorStrategies': strategies = value || []; break;
case 'garageDoorMistakes': mistakes = value || []; break;
case 'garageDoorPartners': partners = value || []; break;
case 'garageDoorPayments': payments = value || []; break;
case 'garageDoorFollowups': followups = value || []; break;
case 'garageDoorReminders': reminders = value || []; break;
case 'garageDoorTechDetails': techDetails = value || {}; break;
case 'garageDoorPartnerExperiences': partnerExperiences = value || {}; break;
case 'garageDoorCustomServices': customServices = value || []; break;
case 'garageDoorCompanies':
companies = (value && value.length) ? value : [{ id: 1, name: 'Murrieta Garage Doors', isDefault: true }];
break;
case 'garageDoorOfferCategories': offerCategories = value || []; break;
case 'garageDoorSettings': settings = mergeSettings(value); break;
default: break;
}
}
function fbApplySnapshot(payload) {
if (!payload || typeof payload !== 'object') return;
fbSuppress = true; // don't echo these writes back up
try {
FB_DATA_KEYS.forEach(key => {
if (!Object.prototype.hasOwnProperty.call(payload, key)) return;
fbAssign(key, payload[key]);
});
// Write through to localStorage from the (normalised) in-memory values
const now = fbSnapshot();
FB_DATA_KEYS.forEach(key => {
localStorage.setItem(key, JSON.stringify(now[key]));
});
} finally {
fbSuppress = false;
}
}
function fbRefreshAll() {
const fns = [
updateLeadDropdown, updateTechnicianDropdown, updatePartnerDropdown,
updateCustomerDropdown, updateCompanyDropdown, updateCompletedJobsList,
updatePerformanceStats, updateCustomerList, updateAllOffersList,
updateTechniciansList, updateStrategiesList, updateMistakesList,
updatePartnersList, renderCalendar, updateUpcomingReminders,
updateMyAccount, updateCharts, initializeSettings, generateOfferDropdown,
updateOfferCategoriesDropdown, updateOfferCategoriesList,
updateCustomServiceList, applyFieldVisibility, applyJobFieldVisibility,
applyTabVisibility
];
fns.forEach(fn => {
try { fn(); } catch (e) { console.warn('Cloud sync refresh failed in', fn.name, e); }
});
}
// ---------- status UI ----------
function fbSetStatus(kind, message) {
const badge = document.getElementById('fb-status-badge');
const text = document.getElementById('fb-status-text');
if (!badge || !text) return;
const map = {
off: ['fb-badge-off', 'Not connected'],
connecting: ['fb-badge-warn', 'Connecting'],
ready: ['fb-badge-ok', 'Connected'],
syncing: ['fb-badge-warn', 'Syncing'],
offline: ['fb-badge-warn', 'Offline'],
error: ['fb-badge-error', 'Error']
};
const entry = map[kind] || map.off;
badge.className = 'fb-badge ' + entry[0];
badge.innerHTML = '<i class="fas fa-circle"></i> ' + entry[1];
text.textContent = message || '';
}
function fbRenderStatus() {
const email = document.getElementById('fb-user-email');
const last = document.getElementById('fb-last-sync');
const conn = document.getElementById('fb-conn-state');
const pending = document.getElementById('fb-pending-state');
if (email) email.textContent = fbUser ? (fbUser.email || fbUser.uid) : '\u2014';
if (last) last.textContent = fbState.lastSync ? new Date(fbState.lastSync).toLocaleString() : 'Never';
if (conn) conn.textContent = fbOnline ? 'Online' : 'Offline';
if (pending) pending.textContent = fbState.pending ? 'Waiting to upload' : 'None';
if (!fbApp) fbSetStatus('off', 'Paste your Firebase config below to get started.');
else if (!fbUser) fbSetStatus('connecting', 'Firebase ready \u2014 sign in with Google to start syncing.');
else if (!fbOnline) fbSetStatus('offline', 'Signed in. Changes are queued and will upload when the network returns.');
else fbSetStatus('ready', 'Syncing to leadManagement/' + fbUser.uid);
}
// ---------- SDK loading ----------
function fbLoadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector('script[data-fb-src="' + src + '"]')) return resolve();
const s = document.createElement('script');
s.src = src;
s.setAttribute('data-fb-src', src);
s.onload = () => resolve();
s.onerror = () => reject(new Error('Could not load ' + src));
document.head.appendChild(s);
});
}
async function fbLoadSdk() {
if (window.firebase && window.firebase.database) return;
await fbLoadScript(FB_SDK_BASE + 'firebase-app-compat.js');
await fbLoadScript(FB_SDK_BASE + 'firebase-auth-compat.js');
await fbLoadScript(FB_SDK_BASE + 'firebase-database-compat.js');
}
// ---------- connect / disconnect ----------
async function fbConnect(silent) {
const cfg = fbGetConfig();
if (!cfg || !cfg.apiKey || !cfg.databaseURL) {
fbSetStatus('error', 'Config is missing apiKey or databaseURL.');
if (!silent) alert('Please paste a valid Firebase config that includes apiKey and databaseURL.');
return;
}
try {
fbSetStatus('connecting', 'Loading the Firebase SDK\u2026');
await fbLoadSdk();
fbApp = firebase.apps.length ? firebase.app() : firebase.initializeApp(cfg);
fbAuth = firebase.auth();
fbDb = firebase.database();
fbAuth.onAuthStateChanged(user => {
fbUser = user;
if (user) fbAttach(); else fbDetach();
fbRenderStatus();
});
fbConnRef = fbDb.ref('.info/connected');
fbConnRef.on('value', snap => {
fbOnline = snap.val() === true;
if (fbOnline && fbState.pending && fbRef) fbPushNow();
fbRenderStatus();
});
fbRenderStatus();
} catch (e) {
fbApp = null;
fbSetStatus('error', e.message);
if (!silent) alert('Could not connect to Firebase: ' + e.message);
}
}
function fbAttach() {
if (!fbDb || !fbUser) return;
fbDetachRef();
fbRef = fbDb.ref('leadManagement/' + fbUser.uid);
fbRef.on('value', snap => {
const v = snap.val();
if (!v || !v.payload) { // nothing in the cloud yet — seed it
fbPushNow();
return;
}
if (v.device === fbSessionId) return; // our own write coming back
fbApplySnapshot(v.payload);
fbState.lastSync = Date.now();
fbSaveState();
fbRefreshAll();
fbRenderStatus();
}, err => {
fbSetStatus('error', 'Read failed: ' + err.message + ' (check your database rules)');
});
}
function fbDetachRef() {
if (fbRef) { try { fbRef.off(); } catch (e) { /* ignore */ } fbRef = null; }
}
function fbDetach() {
fbDetachRef();
fbRenderStatus();
}
// ---------- push / pull ----------
function fbSchedulePush() {
if (!fbState.autoSync) return;
if (!fbRef) { fbState.pending = true; fbSaveState(); fbRenderStatus(); return; }
clearTimeout(fbPushTimer);
fbPushTimer = setTimeout(fbPushNow, 1000);
}
function fbPushNow() {
if (!fbRef) {
fbState.pending = true;
fbSaveState();
fbRenderStatus();
return;
}
clearTimeout(fbPushTimer);
fbSetStatus('syncing', 'Uploading changes\u2026');
fbLastPushAt = Date.now();
fbRef.set({
payload: fbSnapshot(),
updatedAt: fbLastPushAt,
device: fbSessionId
}).then(() => {
fbState.pending = false;
fbState.lastSync = Date.now();
fbSaveState();
fbRenderStatus();
}).catch(err => {
fbState.pending = true;
fbSaveState();
fbSetStatus('error', 'Upload failed: ' + err.message);
});
}
function fbPullNow() {
if (!fbRef) { alert('Connect and sign in first.'); return; }
fbSetStatus('syncing', 'Downloading from the cloud\u2026');
fbRef.once('value').then(snap => {
const v = snap.val();
if (!v || !v.payload) {
fbSetStatus('ready', 'Nothing stored in the cloud yet.');
return;
}
if (!confirm('Replace the data on this device with the cloud copy?')) {
fbRenderStatus();
return;
}
fbApplySnapshot(v.payload);
fbState.lastSync = Date.now();
fbSaveState();
fbRefreshAll();
fbRenderStatus();
alert('Pulled from the cloud successfully.');
}).catch(err => fbSetStatus('error', 'Download failed: ' + err.message));
}
// ---------- button handlers ----------
function fbSaveAndConnect() {
const raw = document.getElementById('fb-config').value;
const cfg = fbParseConfig(raw);
if (!cfg || !cfg.apiKey) {
alert('That config could not be parsed. Paste the whole firebaseConfig object, including apiKey and databaseURL.');
return;
}
if (!cfg.databaseURL) {
alert('The config has no databaseURL. Create a Realtime Database in the Firebase console and copy the config again.');
return;
}
localStorage.setItem(FB_CFG_KEY, JSON.stringify(cfg));
if (fbApp) {
alert('Config saved. Reload the page to reinitialise Firebase with the new settings.');
return;
}
fbConnect(false);
}
function fbSignIn() {
if (!fbAuth) { alert('Save your config and connect first.'); return; }
const provider = new firebase.auth.GoogleAuthProvider();
fbAuth.signInWithPopup(provider).catch(err => {
fbSetStatus('error', 'Sign-in failed: ' + err.message);
alert('Sign-in failed: ' + err.message);
});
}
function fbSignOut() {
if (!fbAuth) return;
fbAuth.signOut().then(() => {
fbDetach();
fbRenderStatus();
});
}
function fbDisconnect() {
if (!confirm('Disconnect cloud sync on this device? Your local data stays put.')) return;
fbDetachRef();
if (fbConnRef) { try { fbConnRef.off(); } catch (e) { /* ignore */ } fbConnRef = null; }
if (fbAuth) { try { fbAuth.signOut(); } catch (e) { /* ignore */ } }
fbState.autoConnect = false;
fbSaveState();
const box = document.getElementById('fb-auto-connect');
if (box) box.checked = false;
fbUser = null;
fbOnline = false;
fbRenderStatus();
fbSetStatus('off', 'Disconnected. Local data is untouched.');
}
function fbToggleAutoSync() {
fbState.autoSync = document.getElementById('fb-auto-sync').checked;
fbSaveState();
if (fbState.autoSync && fbState.pending) fbPushNow();
}
function fbToggleAutoConnect() {
fbState.autoConnect = document.getElementById('fb-auto-connect').checked;
fbSaveState();
}
// ---------- change detection ----------
// Wrapping localStorage means every existing save path in the app feeds
// the sync queue without touching ~50 individual call sites.
(function fbHookStorage() {
const origSet = localStorage.setItem.bind(localStorage);
const origRemove = localStorage.removeItem.bind(localStorage);
localStorage.setItem = function (key, value) {
origSet(key, value);
if (!fbSuppress && FB_DATA_KEYS.indexOf(key) !== -1) fbSchedulePush();
};
localStorage.removeItem = function (key) {
origRemove(key);
if (!fbSuppress && FB_DATA_KEYS.indexOf(key) !== -1) fbSchedulePush();
};
})();
// ---------- startup ----------
document.addEventListener('DOMContentLoaded', function () {
const cfg = fbGetConfig();
const cfgBox = document.getElementById('fb-config');
if (cfgBox && cfg) cfgBox.value = JSON.stringify(cfg, null, 2);
const autoSyncBox = document.getElementById('fb-auto-sync');
if (autoSyncBox) autoSyncBox.checked = fbState.autoSync !== false;
const autoConnBox = document.getElementById('fb-auto-connect');
if (autoConnBox) autoConnBox.checked = fbState.autoConnect === true;
fbRenderStatus();
if (cfg && fbState.autoConnect) fbConnect(true);
});
window.fbSaveAndConnect = fbSaveAndConnect;
window.fbSignIn = fbSignIn;
window.fbSignOut = fbSignOut;
window.fbDisconnect = fbDisconnect;
window.fbPushNow = fbPushNow;
window.fbPullNow = fbPullNow;
window.fbToggleAutoSync = fbToggleAutoSync;
window.fbToggleAutoConnect = fbToggleAutoConnect;
</script>
</body>
</html>