PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / templates / quotes / form.php

form.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.1, at templates/quotes/form.php

1,795 lines 94.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Form Template
4 *
5 * @package EasyInvoice
6 * @since 1.0.0
7 */
8
9 // Only enqueue scripts if not loaded via AJAX
10 if (!wp_doing_ajax()) {
11 // Enqueue CSS and JS files
12 wp_enqueue_style('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/css/invoice-form.css', array(), EASY_INVOICE_VERSION);
13 wp_enqueue_script('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/js/invoice-form.js', array('jquery'), EASY_INVOICE_VERSION, true);
14 wp_enqueue_script('easy-invoice-builder', plugin_dir_url(__FILE__) . '../../assets/js/invoice-builder.js', array('jquery', 'easy-invoice-form'), EASY_INVOICE_VERSION, true);
15 wp_enqueue_script('easy-invoice-payment', plugin_dir_url(__FILE__) . '../../assets/js/payment-manager.js', array('jquery'), EASY_INVOICE_VERSION, true);
16 wp_enqueue_script('easy-quote-save', plugin_dir_url(__FILE__) . '../../assets/js/quote-save.js', array('jquery', 'easy-invoice-form', 'easy-invoice-payment'), EASY_INVOICE_VERSION, true);
17 }
18
19 // Trigger form initialization to allow pro version to register new elements
20 do_action('easy_invoice_form_init');
21
22 // Initialize payment manager with quote settings
23 wp_localize_script('easy-invoice-payment', 'easyInvoicePayment', array(
24 'discountType' => $quote ? $quote->getDiscountType() : 'percentage',
25 'discountValue' => $quote ? $quote->getDiscountValue() : 0,
26 'taxRate' => $quote ? $quote->getTaxRate() : easy_invoice_get_tax_rate(),
27 'calculationMethod' => $quote ? $quote->getCalculationMethod() : 'before_tax',
28 'currency' => $quote ? $quote->getCurrencyCode() : 'USD',
29 'currencySymbol' => $quote ? $quote->getCurrencySymbol() : '$'
30 ));
31
32 // Get tabs from form manager and override labels for quotes
33 $tabs = $quote_form_manager->getTabs();
34
35 // Deduplicate tabs by ID to prevent duplicate tabs in UI
36 $unique_tabs = [];
37 foreach ($tabs as $tab) {
38 $unique_tabs[$tab['id']] = $tab;
39 }
40 $tabs = array_values($unique_tabs);
41
42 // Override tab labels for quotes
43 foreach ($tabs as &$tab) {
44 switch ($tab['id']) {
45 case 'quote-tab':
46 $tab['label'] = __('Quote Details', 'easy-invoice');
47 break;
48 case 'items-tab':
49 $tab['label'] = __('Items', 'easy-invoice');
50 break;
51 case 'client-tab':
52 $tab['label'] = __('Client', 'easy-invoice');
53 break;
54 case 'discounts-taxes-tab':
55 $tab['label'] = __('Discounts & Taxes', 'easy-invoice');
56 break;
57 case 'payments-tab':
58 $tab['label'] = __('Payments', 'easy-invoice');
59 break;
60 }
61 }
62 unset($tab);
63
64 $templates = $quote_form_manager->getTemplates();
65
66 // Use the actual templates from the quote form manager
67 $all_templates = $templates;
68
69
70
71 // Load client data directly in PHP if quote has a client
72 $client_data = null;
73 if ($quote && $quote->getClientId()) {
74 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
75 if ($client) {
76 $client_data = array(
77 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
78 'email' => $client->getEmail() ?: '',
79 'phone' => $client->getExtraInfo() ?: '',
80 'company' => $client->getBusinessClientName() ?: '',
81 'address' => $client->getAddress() ?: '',
82 'website' => $client->getWebsite() ?: '',
83 );
84 }
85 }
86 ?>
87
88 <style>
89 /* Quote Item Collapse/Expand Styling - Exact copy of invoice styling */
90 .quote-item.collapsed-item {
91 padding: 8px !important;
92 margin-bottom: 8px !important;
93 border: 1px solid #e5e7eb !important;
94 border-radius: 0.375rem;
95 background-color: #f9fafb;
96 transition: all 0.2s ease;
97 }
98
99 .quote-item.collapsed-item .flex.items-center.justify-between {
100 margin-bottom: 0 !important;
101 padding-bottom: 0 !important;
102 border-bottom: none !important;
103 }
104
105 .quote-item.collapsed-item .item-collapsed-summary {
106 display: inline-flex !important;
107 align-items: center;
108 font-size: 0.875rem;
109 color: #6b7280;
110 }
111
112 .quote-item .item-content {
113 transition: all 0.3s ease-in-out;
114 }
115
116 .quote-item.collapsed-item .item-content {
117 display: none;
118 }
119
120 /* Additional styling for quote items to match invoice behavior */
121 .quote-item .item-collapsed-summary {
122 display: none;
123 background-color: #f8fafc;
124 border: 1px solid #e2e8f0;
125 border-radius: 9999px;
126 padding: 0.25rem 0.75rem;
127 font-size: 0.875rem;
128 color: #64748b;
129 font-weight: 500;
130 }
131
132 .quote-item.collapsed-item .item-collapsed-summary {
133 display: inline-flex !important;
134 }
135
136 .quote-item .fill-sample-data-btn {
137 transition: opacity 0.2s ease-in-out;
138 }
139
140 .quote-item.collapsed-item .fill-sample-data-btn {
141 opacity: 0;
142 pointer-events: none;
143 }
144
145 /* Smooth transitions for collapse/expand */
146 .quote-item .item-collapse-toggle {
147 transition: all 0.2s ease-in-out;
148 }
149
150 .quote-item .item-collapse-toggle:hover {
151 color: #4f46e5 !important;
152 transform: scale(1.1);
153 }
154
155 /* Premium Template Styling */
156 .premium-template-blurred {
157 position: relative;
158 opacity: 0.8;
159 transition: all 0.4s ease;
160 cursor: pointer;
161 border: 2px solid #e5e7eb;
162 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
163 transform: scale(0.98);
164 }
165
166 .premium-template-blurred::before {
167 content: '';
168 position: absolute;
169 top: 0;
170 left: 0;
171 right: 0;
172 bottom: 0;
173 background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%);
174 border-radius: 0.5rem;
175 z-index: 1;
176 pointer-events: none;
177 }
178
179 .premium-template-blurred::after {
180 content: 'PRO';
181 position: absolute;
182 top: 8px;
183 right: 8px;
184 background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
185 color: white;
186 font-size: 10px;
187 font-weight: 700;
188 padding: 2px 6px;
189 border-radius: 4px;
190 z-index: 2;
191 box-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);
192 letter-spacing: 0.5px;
193 }
194
195 .premium-template-blurred:hover {
196 opacity: 0.95;
197 transform: scale(1.02);
198 border-color: #8b5cf6;
199 box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
200 }
201
202 .premium-template-blurred:hover::after {
203 background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%);
204 box-shadow: 0 4px 8px rgba(139, 92, 246, 0.4);
205 }
206
207 .premium-template-blurred .template-radio {
208 pointer-events: none;
209 }
210
211 .premium-template-blurred > div {
212 position: relative;
213 z-index: 3;
214 }
215 </style>
216
217 <?php if (wp_doing_ajax()): ?>
218 <form id="quote-form" method="post">
219 <input type="hidden" id="quote-id" name="quote_id" value="<?php echo $quote_id ? esc_attr($quote_id) : ''; ?>">
220 <?php endif; ?>
221
222 <div class="bg-white shadow rounded-lg">
223 <div class="border-b border-gray-200">
224 <nav class="flex -mb-px" aria-label="<?php esc_attr_e( 'Quote form sections', 'easy-invoice' ); ?>">
225 <?php foreach ($tabs as $tab): ?>
226 <button type="button"
227 class="tab-button whitespace-nowrap py-3 px-6 border-b-2 <?php echo $tab['active'] ? 'border-indigo-500 text-sm text-indigo-600' : 'border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?> flex flex-col items-center"
228 data-tab="<?php echo esc_attr($tab['id']); ?>">
229 <i class="<?php echo esc_attr($tab['icon']); ?> mb-1 text-lg"></i>
230 <span><?php echo esc_html($tab['label']); ?></span>
231 </button>
232 <?php endforeach; ?>
233 </nav>
234 </div>
235
236
237 <?php foreach ($tabs as $tab): ?>
238 <div class="tab-content <?php echo $tab['active'] ? 'active' : 'hidden'; ?> px-6 py-5" id="<?php echo esc_attr($tab['id']); ?>">
239 <?php if ($tab['id'] === 'quote-tab'): ?>
240 <div class="mb-8 pb-6 border-b border-gray-200">
241 <div class="flex justify-between items-center mb-4">
242 <h3 class="text-lg font-medium text-gray-900"><?php _e('Quote Template', 'easy-invoice'); ?></h3>
243 <button type="button" id="browse-all-templates" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
244 <span class="mr-1"><?php _e('Browse Quote Templates', 'easy-invoice'); ?></span>
245 <i class="fas fa-chevron-right"></i>
246 </button>
247 </div>
248
249 <?php
250 // Get the current template (default to 'standard')
251 $current_template = $quote ? $quote->getTemplate() : 'standard';
252
253 if (empty($current_template)) {
254 $current_template = 'standard';
255 }
256
257 // Always include the hidden input for the template field
258 ?>
259 <input type="hidden" name="quote_template" value="<?php echo esc_attr($current_template); ?>">
260 <?php
261
262 // Check if this is the free version
263 $is_free_version = !easy_invoice_has_pro();
264
265
266
267 // Show all templates grid
268 ?>
269 <div class="grid grid-cols-2 md:grid-cols-3 gap-4" id="main-templates-grid">
270 <?php
271 // Build the main grid templates array - always include current template plus 2 others
272 $main_grid_templates = [];
273
274 // First, add the current template if it exists in all_templates
275 if (isset($all_templates[$current_template])) {
276 // For free users, if current template is premium, default to standard
277 if ($is_free_version && isset($all_templates[$current_template]['is_free']) && !$all_templates[$current_template]['is_free']) {
278 $current_template = 'standard';
279 $main_grid_templates['standard'] = $all_templates['standard'];
280 } else {
281 $main_grid_templates[$current_template] = $all_templates[$current_template];
282 }
283 }
284
285 // Add other templates to fill up to 3, prioritizing standard templates
286 $default_templates = ['standard', 'legacy', 'modern'];
287 foreach ($default_templates as $template_id) {
288 if (count($main_grid_templates) >= 3) break;
289 if ($template_id !== $current_template && isset($all_templates[$template_id])) {
290 $main_grid_templates[$template_id] = $all_templates[$template_id];
291 }
292 }
293
294 // If we still don't have 3 templates, add any remaining templates
295 foreach ($all_templates as $template_id => $template) {
296 if (count($main_grid_templates) >= 3) break;
297 if (!isset($main_grid_templates[$template_id])) {
298 $main_grid_templates[$template_id] = $template;
299 }
300 }
301
302 // Display the main grid templates
303 foreach ($main_grid_templates as $template_id => $template) :
304 $checked = ($current_template === $template_id) ? 'checked' : '';
305 $selected_class = ($current_template === $template_id) ? 'selected' : '';
306
307 // Check if this template is premium (not free)
308 $is_premium = isset($template['is_free']) && !$template['is_free'];
309
310 // Add premium overlay classes
311 $premium_classes = '';
312 if ($is_free_version && $is_premium) {
313 $premium_classes = 'premium-template-blurred';
314 }
315 ?>
316 <div class="template-card <?php echo $selected_class . ' ' . $premium_classes; ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo $is_premium ? 'true' : 'false'; ?>">
317 <input type="radio" id="template-<?php echo esc_attr($template_id); ?>" name="quote_template" value="<?php echo esc_attr($template_id); ?>" class="template-radio" <?php echo $checked; ?> <?php echo ($is_free_version && $is_premium) ? 'disabled' : ''; ?>>
318 <div class="p-4 border rounded-lg">
319 <div class="flex items-center mb-2">
320 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
321 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
322 </div>
323 <p class="text-sm text-gray-600"><?php echo esc_html($template['description']); ?></p>
324 <div class="mt-2 flex-grow">
325 <div class="template-preview bg-gray-100 rounded h-16 flex items-center justify-center <?php echo esc_attr($template_id); ?>">
326 </div>
327 </div>
328 </div>
329 </div>
330 <?php endforeach; ?>
331 </div>
332 </div>
333
334 <div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
335 <?php
336 $main_fields = $quote_form_manager->getFields('quote-tab');
337
338
339
340 foreach ($main_fields as $field):
341 // Skip fields that have a section (like notes) - they're rendered separately
342 // Also skip the template field as it's rendered manually in the template selection section
343 if (!isset($field['section']) && $field['name'] !== 'quote_template') {
344
345 echo $quote_form_manager->renderField($field, $quote);
346 }
347 endforeach;
348 ?>
349 </div>
350
351 <?php if (!$quote): ?>
352 <div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
353 <div class="flex">
354 <div class="flex-shrink-0">
355 <i class="fas fa-info-circle text-blue-400"></i>
356 </div>
357 <div class="ml-3">
358 <p class="text-sm text-blue-700">
359 <?php _e('Quote number will be automatically generated when you save the quote.', 'easy-invoice'); ?>
360 </p>
361 </div>
362 </div>
363 </div>
364 <?php endif; ?>
365
366 <div class="mt-8 border-t border-gray-200 pt-6">
367 <h3 class="text-lg font-medium text-gray-900"><?php _e('Notes & Terms', 'easy-invoice'); ?></h3>
368 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
369 <?php
370 // Only render notes and terms fields, not all quote tab fields
371 $notes_fields = $quote_form_manager->getFieldsBySection('quote-tab', 'notes');
372 foreach ($notes_fields as $field):
373 echo $quote_form_manager->renderField($field, $quote);
374 endforeach;
375 ?>
376 </div>
377 </div>
378
379 <?php elseif ($tab['id'] === 'items-tab'): ?>
380 <div class="flex justify-between items-center mb-6">
381 <h2 class="text-lg font-medium text-gray-900"><?php _e('Quote Items', 'easy-invoice'); ?></h2>
382 <button type="button" id="collapse-all-items" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
383 <i class="fas fa-chevron-down mr-1"></i>
384 <span><?php _e('Collapse All', 'easy-invoice'); ?></span>
385 </button>
386 </div>
387
388 <div class="space-y-4 quote-items-container">
389 <?php
390 // If editing an existing quote, directly output quote items
391 if ($quote && $quote->getItems()) :
392 $index = 0;
393 foreach ($quote->getItems() as $item) :
394 // Generate a unique ID for the item
395 $item_id = 'item_' . time() . '_' . $index . '_' . rand(1000, 9999);
396
397 // Get display title for header using dynamic field access
398 $title = $item->getName();
399
400
401 if (empty($title) && method_exists($item, 'getData')) {
402 $item_data = $item->getData();
403 if (is_array($item_data) && isset($item_data['title'])) {
404 $title = $item_data['title'];
405 }
406 }
407
408 $display_title = !empty($title) ? $title : sprintf(__('Item #%d', 'easy-invoice'), $index + 1);
409 $short_title = strlen($display_title) > 30 ? substr($display_title, 0, 30) . '...' : $display_title;
410 ?>
411 <div class="quote-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;" id="<?php echo $item_id; ?>">
412 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
413 <div class="flex items-center">
414 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
415 <i class="fas fa-chevron-down text-sm"></i>
416 </button>
417 <h3 class="text-base font-normal text-gray-800 flex-1">
418 <?php
419 echo !empty($title) ? esc_html($title) : __('New Item', 'easy-invoice');
420 ?>
421 </h3>
422 </div>
423 <div class="flex items-center gap-3">
424 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
425 <span class="quantity-summary"><?php echo esc_html($item->getQuantity()); ?></span> × $<span class="price-summary"><?php echo esc_html(number_format(floatval($item->getPrice()), 2)); ?></span>
426 = $<span class="total-summary font-medium"><?php echo esc_html(number_format($item->getAmount(), 2)); ?></span>
427 </div>
428 <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
429 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
430 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
431 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
432 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
433 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
434 </svg>
435 </button>
436 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
437 <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
438 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
439 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
440 </svg>
441 </button>
442 <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="<?php echo $index; ?>" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
443 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
444 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
445 </svg>
446 </button>
447 <?php endif; ?>
448 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
449 <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
450 </button>
451 </div>
452 </div>
453 <div class="item-content">
454 <?php echo $quote_form_manager->generateExistingItemHtml($item, $index); ?>
455 </div>
456 </div>
457 <?php
458 $index++;
459 endforeach;
460 else:
461 // For new quotes, render one empty item by default
462 $index = 0;
463 // Get the item fields config
464 $item_fields = $quote_form_manager->getItemFields();
465 $title_placeholder = '';
466 foreach ($item_fields as $field) {
467 if ($field['name'] === 'title') {
468 $title_placeholder = $field['placeholder'] ?? __('Item', 'easy-invoice');
469 break;
470 }
471 }
472 // Try to get the value from POST (if form was submitted and errored)
473 $item_title = isset($_POST['items'][0]['title']) ? trim($_POST['items'][0]['title']) : '';
474 ?>
475 <div class="quote-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;">
476 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
477 <div class="flex items-center">
478 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
479 <i class="fas fa-chevron-down text-sm"></i>
480 </button>
481 <h3 class="text-base font-normal text-gray-800 flex-1">
482 <?php
483 echo !empty($item_title) ? esc_html($item_title) : esc_html($title_placeholder);
484 ?>
485 </h3>
486 </div>
487 <div class="flex items-center gap-3">
488 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
489 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
490 = $<span class="total-summary font-medium">0.00</span>
491 </div>
492 <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
493 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
494 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
495 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
496 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
497 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
498 </svg>
499 </button>
500 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
501 <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
502 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
503 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
504 </svg>
505 </button>
506 <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="0" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
507 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
508 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
509 </svg>
510 </button>
511 <?php endif; ?>
512 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
513 <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
514 </button>
515 </div>
516 </div>
517 <div class="item-content">
518 <?php echo $quote_form_manager->generateItemTemplateHtml(); ?>
519 </div>
520 </div>
521 <?php endif; ?>
522 </div>
523
524 <div class="mt-6">
525 <button type="button" class="add-item-button w-full flex justify-center items-center px-6 py-4 bg-white border border-blue-100 rounded-xl text-indigo-700 hover:bg-blue-50 hover:border-blue-200 hover:shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 group">
526 <div class="flex items-center">
527 <div class="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center mr-3 group-hover:bg-indigo-200 transition-colors">
528 <i class="fas fa-plus text-indigo-600 text-lg"></i>
529 </div>
530 <div class="text-left">
531 <span class="font-semibold text-base"><?php _e('Add Another Item', 'easy-invoice'); ?></span>
532 <p class="text-sm text-indigo-600 mt-0.5"><?php _e('Click to add a new line item', 'easy-invoice'); ?></p>
533 </div>
534 </div>
535 </button>
536 </div>
537
538 <?php elseif ($tab['id'] === 'client-tab'): ?>
539 <div class="mb-6">
540 <label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('Select Existing Client', 'easy-invoice'); ?></label>
541
542 <div class="relative">
543 <div class="flex items-center space-x-3">
544 <div class="relative flex-grow">
545 <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
546 <i class="fas fa-search text-gray-400"></i>
547 </div>
548 <input
549 type="text"
550 id="client-search-input"
551 class="block w-full rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 pl-10"
552 placeholder="<?php _e('Search clients by name, email, or company...', 'easy-invoice'); ?>"
553 autocomplete="off"
554 >
555 </div>
556 <button type="button" id="add-new-client-btn" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
557 <i class="fas fa-plus -ml-1 mr-2"></i>
558 <?php _e('New Client', 'easy-invoice'); ?>
559 </button>
560 </div>
561
562 <div id="client-search-results" class="hidden absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-80 overflow-y-auto">
563 <div id="client-search-loading" class="hidden p-6 text-center text-gray-500">
564 <i class="fas fa-spinner fa-spin mr-2"></i>
565 <?php _e('Searching...', 'easy-invoice'); ?>
566 </div>
567 <div id="client-search-empty" class="hidden p-6 text-center text-gray-500">
568 <i class="fas fa-search mr-2"></i>
569 <?php _e('No clients found', 'easy-invoice'); ?>
570 </div>
571 <div id="client-search-list" class="py-2">
572 </div>
573 </div>
574
575 <div id="selected-client-display" class="mt-4 p-4 bg-gradient-to-r from-indigo-50 to-blue-50 border border-indigo-200 rounded-lg shadow-sm <?php echo ($quote && $quote->getClientId()) ? '' : 'hidden'; ?>">
576 <div class="flex items-center justify-between">
577 <div class="flex items-center">
578 <div class="flex-shrink-0">
579 <div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full flex items-center justify-center shadow-sm">
580 <i class="fas fa-user text-white text-sm"></i>
581 </div>
582 </div>
583 <div class="ml-4">
584 <p id="selected-client-name" class="text-sm font-semibold text-gray-900">
585 <?php
586 if ($quote && $quote->getClientId()) {
587 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
588 if ($client) {
589 echo esc_html($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
590 } else {
591 echo '-';
592 }
593 } else {
594 echo '-';
595 }
596 ?>
597 </p>
598 <p id="selected-client-email" class="text-sm text-gray-600">
599 <?php
600 if ($quote && $quote->getClientId()) {
601 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
602 if ($client) {
603 echo esc_html($client->getEmail() ?: '-');
604 } else {
605 echo '-';
606 }
607 } else {
608 echo '-';
609 }
610 ?>
611 </p>
612 </div>
613 </div>
614 <button type="button" id="clear-client-selection" class="text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-full hover:bg-white hover:shadow-sm">
615 <i class="fas fa-times"></i>
616 </button>
617 </div>
618 </div>
619 </div>
620
621 <select id="select-client" class="hidden">
622 <option value=""><?php _e('-- Select a client --', 'easy-invoice'); ?></option>
623 <?php
624 // Output client options
625 if (!empty($clients)) {
626 foreach ($clients as $client) {
627 // Get the WordPress user data directly
628 $user = get_user_by('id', $client->getId());
629 if (!$user) {
630 continue;
631 }
632
633 // Use standard WordPress user fields
634 $business_name = $client->business_client_name ?: '';
635 $first_name = $user->first_name ?: '';
636 $last_name = $user->last_name ?: '';
637 $client_email = $user->user_email ?: '';
638
639 // Create display name
640 $client_name = $business_name ?: ($first_name . ' ' . $last_name);
641 if (empty(trim($client_name))) {
642 $client_name = $user->display_name ?: 'User ' . $client->getId();
643 }
644
645 $selected = $quote && $quote->getClientId() == $client->getId() ? 'selected' : '';
646 printf(
647 '<option value="%s" %s data-name="%s" data-email="%s">%s (%s)</option>',
648 esc_attr($client->getId()),
649 $selected,
650 esc_attr($client_name),
651 esc_attr($client_email),
652 esc_html($client_name),
653 esc_html($client_email)
654 );
655 }
656 }
657 ?>
658 </select>
659 </div>
660
661 <div class="border-t border-gray-200 pt-4 mt-4">
662 <input type="hidden" name="client_id" id="client-id" value="<?php echo $quote ? esc_attr($quote->getClientId()) : ''; ?>">
663
664 <div id="client-info-display" class="mt-6 <?php echo $quote && $quote->getClientId() ? '' : 'hidden'; ?>">
665 <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
666 <div class="flex items-center justify-between pb-4 border-b border-gray-200 mb-4">
667 <h4 class="text-lg font-semibold text-gray-800 flex items-center">
668 <i class="fas fa-user-check text-indigo-500 mr-3"></i>
669 <span><?php _e('Client Information', 'easy-invoice'); ?></span>
670 </h4>
671 <a href="<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id=' . ($quote && $quote->getClientId() ? $quote->getClientId() : '')); ?>"
672 id="edit-selected-client"
673 class="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
674 target="_blank">
675 <i class="fas fa-pen mr-1"></i>
676 <?php _e('Edit', 'easy-invoice'); ?>
677 </a>
678 </div>
679
680 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
681 <div class="flex items-start group">
682 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-user-tie text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
683 <div class="ml-3">
684 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Name', 'easy-invoice'); ?></label>
685 <p id="display-client-name" class="text-sm font-semibold text-gray-800 mt-1">
686 <?php
687 if ($quote && $quote->getClientId()) {
688 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
689 if ($client) {
690 echo esc_html($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
691 } else {
692 echo '-';
693 }
694 } else {
695 echo '-';
696 }
697 ?>
698 </p>
699 </div>
700 </div>
701
702 <div class="flex items-start group">
703 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-building text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
704 <div class="ml-3">
705 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Company', 'easy-invoice'); ?></label>
706 <p id="display-client-company" class="text-sm font-semibold text-gray-800 mt-1">
707 <?php
708 if ($quote && $quote->getClientId()) {
709 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
710 if ($client) {
711 echo esc_html($client->getBusinessClientName() ?: '-');
712 } else {
713 echo '-';
714 }
715 } else {
716 echo '-';
717 }
718 ?>
719 </p>
720 </div>
721 </div>
722
723 <div class="flex items-start group">
724 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-envelope text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
725 <div class="ml-3">
726 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Email', 'easy-invoice'); ?></label>
727 <p id="display-client-email" class="text-sm text-gray-800 mt-1 break-all">
728 <?php
729 if ($quote && $quote->getClientId()) {
730 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
731 if ($client) {
732 echo esc_html($client->getEmail() ?: '-');
733 } else {
734 echo '-';
735 }
736 } else {
737 echo '-';
738 }
739 ?>
740 </p>
741 </div>
742 </div>
743
744 <div class="flex items-start group">
745 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-phone-alt text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
746 <div class="ml-3">
747 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Phone', 'easy-invoice'); ?></label>
748 <p id="display-client-phone" class="text-sm text-gray-800 mt-1">
749 <?php
750 if ($quote && $quote->getClientId()) {
751 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
752 if ($client) {
753 echo esc_html($client->getExtraInfo() ?: esc_html__('', 'easy-invoice'));
754 } else {
755 echo esc_html__('', 'easy-invoice');
756 }
757 } else {
758 echo esc_html__('', 'easy-invoice');
759 }
760 ?>
761 </p>
762 </div>
763 </div>
764
765 <div class="flex items-start group md:col-span-2">
766 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-globe-americas text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
767 <div class="ml-3">
768 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Website', 'easy-invoice'); ?></label>
769 <p id="display-client-website" class="text-sm text-gray-800 mt-1">
770 <?php
771 if ($quote && $quote->getClientId()) {
772 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
773 if ($client) {
774 echo esc_html($client->getWebsite() ?: esc_html__('', 'easy-invoice'));
775 } else {
776 echo esc_html__('', 'easy-invoice');
777 }
778 } else {
779 echo esc_html__('', 'easy-invoice');
780 }
781 ?>
782 </p>
783 </div>
784 </div>
785
786 <div class="flex items-start group md:col-span-2">
787 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-map-marker-alt text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
788 <div class="ml-3">
789 <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Address', 'easy-invoice'); ?></label>
790 <p id="display-client-address" class="text-sm text-gray-800 mt-1 whitespace-pre-wrap">
791 <?php
792 if ($quote && $quote->getClientId()) {
793 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
794 if ($client) {
795 echo esc_html($client->getAddress() ?: esc_html__('', 'easy-invoice'));
796 } else {
797 echo esc_html__('', 'easy-invoice');
798 }
799 } else {
800 echo esc_html__('', 'easy-invoice');
801 }
802 ?>
803 </p>
804 </div>
805 </div>
806 </div>
807 </div>
808 </div>
809
810 <div id="no-client-message" class="mt-4 text-center py-6 border-2 border-dashed border-gray-300 rounded-lg <?php echo $quote && $quote->getClientId() ? 'hidden' : ''; ?>">
811 <p class="text-gray-600"><?php _e('Select a client from the dropdown above to view their information.', 'easy-invoice'); ?></p>
812 </div>
813 </div>
814
815 <?php elseif ($tab['id'] === 'discounts-taxes-tab'): ?>
816 <div>
817 <h3 class="text-lg font-medium text-gray-900"><?php _e('Discount & Tax Settings', 'easy-invoice'); ?></h3>
818
819 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
820 <?php
821 // Get all discount and tax fields
822 $discount_fields = $quote_form_manager->getFieldsBySection('discounts-taxes-tab', 'discount');
823 $tax_fields = $quote_form_manager->getFieldsBySection('discounts-taxes-tab', 'tax');
824 $additional_tax_fields = $quote_form_manager->getFieldsBySection('discounts-taxes-tab', 'additional_tax');
825
826 // Combine and sort by order
827 $all_fields = array_merge($discount_fields, $tax_fields, $additional_tax_fields);
828 usort($all_fields, function($a, $b) {
829 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
830 });
831
832 foreach ($all_fields as $field):
833 echo $quote_form_manager->renderField($field, $quote);
834 endforeach;
835 ?>
836 </div>
837 </div>
838
839 <?php elseif ($tab['id'] === 'payments-tab'): ?>
840 <div>
841 <h3 class="text-lg font-medium text-gray-900"><?php _e('Payment Information', 'easy-invoice'); ?></h3>
842 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
843 <?php
844 $payment_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'payment');
845 foreach ($payment_fields as $field):
846 echo $quote_form_manager->renderField($field, $quote);
847 endforeach;
848 ?>
849 </div>
850
851 <div class="mt-8 border-t border-gray-200 pt-6">
852 <h3 class="text-lg font-medium text-gray-900"><?php _e('Currency', 'easy-invoice'); ?></h3>
853 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
854 <?php
855 $currency_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'currency');
856 foreach ($currency_fields as $field):
857 echo $quote_form_manager->renderField($field, $quote);
858 endforeach;
859 ?>
860 </div>
861 </div>
862 </div>
863
864 <?php endif; ?>
865 </div>
866 <?php endforeach; ?>
867 </div>
868
869 <div id="template-gallery-modal" style="display: none;" class="fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
870 <div class="mx-auto p-6 border w-full md:w-4/5 max-w-4xl shadow-lg rounded-md bg-white max-h-[80vh] overflow-y-auto">
871 <div class="flex justify-between items-center mb-4">
872 <h3 class="text-xl font-medium text-gray-900"><?php _e('Browse Quote Templates', 'easy-invoice'); ?></h3>
873 <button type="button" class="close-gallery-modal text-gray-400 hover:text-gray-500 focus:outline-none">
874 <span class="sr-only"><?php _e('Close', 'easy-invoice'); ?></span>
875 <i class="fas fa-times text-xl"></i>
876 </button>
877 </div>
878
879 <div class="templates-grid grid grid-cols-2 md:grid-cols-3 gap-4 mb-4 max-h-[45vh] overflow-y-auto p-2">
880 <?php
881 // Display all template options in the modal
882 foreach ($all_templates as $template_id => $template) :
883 $is_current = ($current_template === $template_id);
884 $selected_class = $is_current ? 'selected' : '';
885 ?>
886 <div class="gallery-template-card <?php echo $selected_class; ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo isset($template['is_free']) && !$template['is_free'] ? 'true' : 'false'; ?>">
887 <div class="p-4 border rounded-lg">
888 <div class="flex items-center mb-2">
889 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
890 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
891 </div>
892 <p class="text-sm text-gray-600"><?php echo esc_html($template['description']); ?></p>
893 <div class="mt-2 flex-grow">
894 <div class="template-preview bg-gray-100 rounded h-16 flex items-center justify-center <?php echo esc_attr($template_id); ?>">
895 </div>
896 </div>
897 </div>
898 </div>
899 <?php endforeach; ?>
900 </div>
901
902 <div class="flex justify-end mt-4 space-x-3">
903 <button type="button" id="apply-selected-template" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
904 <?php _e('Apply', 'easy-invoice'); ?>
905 </button>
906 <button type="button" class="gallery-close-btn inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
907 <?php _e('Close', 'easy-invoice'); ?>
908 </button>
909 </div>
910 </div>
911 </div>
912
913 <template id="quote-item-template" style="display: none;">
914 <div class="quote-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;">
915 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
916 <div class="flex items-center">
917 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
918 <i class="fas fa-chevron-down text-sm"></i>
919 </button>
920 <h3 class="text-base font-normal text-gray-800 flex-1">
921 <?php _e('New Item', 'easy-invoice'); ?>
922 </h3>
923 </div>
924 <div class="flex items-center gap-3">
925 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
926 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
927 = $<span class="total-summary">0.00</span>
928 </div>
929 <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
930 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
931 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
932 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
933 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
934 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
935 </svg>
936 </button>
937 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
938 <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
939 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
940 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
941 </svg>
942 </button>
943 <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
944 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
945 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
946 </svg>
947 </button>
948 <?php endif; ?>
949 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
950 <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
951 </button>
952 </div>
953 </div>
954 <div class="item-content">
955 <?php echo $quote_form_manager->generateItemTemplateHtml(); ?>
956 </div>
957 </div>
958 </template>
959
960 <script>
961 // Set Pro detection early
962 window.easyInvoice = window.easyInvoice || {};
963 window.easyInvoice.isPro = <?php echo easy_invoice_has_pro() ? 'true' : 'false'; ?>;
964
965 jQuery(document).ready(function($) {
966 // Set flag to indicate this is a quote form
967 window.isQuoteForm = true;
968
969 // Function to get URL parameter
970 function getUrlParameter(name) {
971 name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
972 var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
973 var results = regex.exec(location.search);
974 return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
975 }
976
977 // Function to switch to a specific tab
978 function switchToTab(tabId) {
979 if (tabId && $('#' + tabId).length) {
980 // Remove active state from all tabs
981 $('.tab-button').removeClass('border-indigo-500 text-indigo-600').addClass('border-transparent text-gray-500');
982 $('.tab-content').addClass('hidden');
983
984 // Activate the specified tab
985 $('.tab-button[data-tab="' + tabId + '"]').addClass('border-indigo-500 text-indigo-600');
986 $('#' + tabId).removeClass('hidden');
987
988 // Update URL without page reload
989 var url = new URL(window.location);
990 url.searchParams.set('tab', tabId);
991 window.history.replaceState({}, '', url);
992 }
993 }
994
995 // Check for tab parameter on page load
996 var initialTab = getUrlParameter('tab');
997 if (initialTab) {
998 switchToTab(initialTab);
999 }
1000
1001 // Tab switching
1002 $('.tab-button').on('click', function() {
1003 var tabId = $(this).data('tab');
1004 switchToTab(tabId);
1005 });
1006
1007 // Template gallery functionality
1008 $('#browse-all-templates, #browse-templates-btn').on('click', function() {
1009 // Check if this is the free version
1010 const isFreeVersion = !window.easyInvoice || !window.easyInvoice.isPro;
1011
1012 if (isFreeVersion) {
1013 // Show premium upgrade modal for free users
1014 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1015 EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Premium Templates', 'easy-invoice'); ?>', '<?php _e('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look. Browse through our complete template library and find the perfect design for your business.', 'easy-invoice'); ?>');
1016 } else {
1017 alert('<?php _e('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice'); ?>');
1018 }
1019 } else {
1020 // Pro users can browse all templates
1021 $('#template-gallery-modal').show();
1022 }
1023 });
1024
1025 $('.close-gallery-modal, .gallery-close-btn').on('click', function() {
1026 $('#template-gallery-modal').hide();
1027 });
1028
1029 // Gallery template card click handler
1030 $(document).on('click', '.gallery-template-card', function() {
1031 $('.gallery-template-card').removeClass('selected');
1032 $(this).addClass('selected');
1033 });
1034
1035 // Template card click handler
1036 $(document).on('click', '.template-card', function(e) {
1037 const templateId = $(this).data('template-id');
1038
1039 // Check if this is a premium template in free version
1040 const isPremium = $(this).attr('data-is-premium') === 'true';
1041 const isFreeVersion = !window.easyInvoice || !window.easyInvoice.isPro;
1042
1043 if (isPremium && isFreeVersion) {
1044 // Show premium upgrade modal instead of selecting the template
1045 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1046 EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Premium Templates', 'easy-invoice'); ?>', '<?php _e('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look.', 'easy-invoice'); ?>');
1047 } else {
1048 alert('<?php _e('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice'); ?>');
1049 }
1050 return;
1051 }
1052
1053 // Select the template
1054 $('.template-card').removeClass('selected');
1055 $(this).addClass('selected');
1056 $('input[name="quote_template"]').val(templateId);
1057 $('input[name="quote_template"][value="' + templateId + '"]').prop('checked', true);
1058
1059 // Update the live preview
1060 if (typeof updateLivePreview === 'function') {
1061 updateLivePreview(templateId);
1062 }
1063 });
1064
1065 // Apply selected template
1066 $('#apply-selected-template').on('click', function() {
1067 const selectedTemplate = $('.gallery-template-card.selected').data('template-id');
1068 if (selectedTemplate) {
1069 $('.template-card').removeClass('selected');
1070 $('.template-card[data-template-id="' + selectedTemplate + '"]').addClass('selected');
1071 $('input[name="quote_template"]').val(selectedTemplate);
1072 $('input[name="quote_template"][value="' + selectedTemplate + '"]').prop('checked', true);
1073 $('#template-gallery-modal').hide();
1074
1075 // Update the live preview
1076 if (typeof updateLivePreview === 'function') {
1077 updateLivePreview(selectedTemplate);
1078 }
1079 }
1080 });
1081
1082
1083
1084
1085
1086 // Quote Items functionality
1087 let itemIndex = $('.quote-item').length;
1088
1089 // Initialize existing items with correct indices
1090 $('.quote-item').each(function(index) {
1091 const $item = $(this);
1092
1093 // Update all input fields - match both items[0][fieldname] and items[-1][fieldname]
1094 $item.find('input[name*="items["]').each(function() {
1095 var oldName = $(this).attr('name');
1096 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + index + '][');
1097 $(this).attr('name', newName);
1098 });
1099
1100 // Update all textarea fields - match both items[0][fieldname] and items[-1][fieldname]
1101 $item.find('textarea[name*="items["]').each(function() {
1102 var oldName = $(this).attr('name');
1103 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + index + '][');
1104 $(this).attr('name', newName);
1105 });
1106
1107 // Update all select fields - match both items[0][fieldname] and items[-1][fieldname]
1108 $item.find('select[name*="items["]').each(function() {
1109 var oldName = $(this).attr('name');
1110 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + index + '][');
1111 $(this).attr('name', newName);
1112 });
1113
1114 // Update field IDs to be unique (handle both _-1 and _0)
1115 $item.find('[id*="_-1"], [id*="_0"]').each(function() {
1116 var oldId = $(this).attr('id');
1117 var newId = oldId.replace(/_(?:-1|0)/, '_' + index);
1118 $(this).attr('id', newId);
1119 // Update corresponding label for attribute
1120 var $label = $item.find('label[for="' + oldId + '"]');
1121 if ($label.length) {
1122 $label.attr('for', newId);
1123 }
1124 });
1125 });
1126
1127 // Add new item
1128 $('.add-item-button').on('click', function() {
1129 addNewQuoteItem();
1130 });
1131
1132 // Remove item
1133 $(document).on('click', '.remove-item', function() {
1134 const itemContainer = $(this).closest('.quote-item');
1135 if ($('.quote-item').length > 1) {
1136 itemContainer.remove();
1137 updateItemNumbers();
1138 calculateTotals();
1139 } else {
1140 // If it's the last item, just clear the fields
1141 itemContainer.find('input, textarea').val('');
1142 itemContainer.find('select').prop('selectedIndex', 0);
1143 }
1144 });
1145
1146 // Item collapse toggle
1147 $(document).on('click', '.item-collapse-toggle', function() {
1148 const itemContainer = $(this).closest('.quote-item');
1149 const content = itemContainer.find('.item-content');
1150 const summary = itemContainer.find('.item-collapsed-summary');
1151 const sampleButton = itemContainer.find('.fill-sample-data-btn');
1152 const icon = $(this).find('i');
1153
1154 if (content.is(':visible')) {
1155 // Collapsing - update summary and change icon
1156 updateQuoteItemSummary(itemContainer);
1157 content.slideUp(200);
1158 summary.slideDown(200);
1159 sampleButton.hide(); // Hide sample button when collapsed
1160 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
1161 // Add compact styling to the collapsed item
1162 itemContainer.addClass('collapsed-item');
1163 } else {
1164 // Expanding - hide summary and change icon
1165 content.slideDown(200);
1166 summary.slideUp(200);
1167 sampleButton.show(); // Show sample button when expanded
1168 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
1169 // Remove compact styling from the expanded item
1170 itemContainer.removeClass('collapsed-item');
1171 }
1172
1173 // Update totals if payment manager is available
1174 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1175 window.EasyInvoicePayment.updateTotals();
1176 } else if (typeof updatePreview === 'function') {
1177 updatePreview();
1178 }
1179 });
1180
1181 // Fill sample data
1182 $(document).on('click', '.fill-sample-data-btn', function() {
1183 const itemContainer = $(this).closest('.quote-item');
1184 fillQuoteItemWithSampleData(itemContainer);
1185 });
1186
1187 // Collapse all items
1188 $('#collapse-all-items').on('click', function() {
1189 const $button = $(this);
1190 let allCollapsed = true;
1191
1192 // Check if all items are already collapsed
1193 $('.quote-item').each(function() {
1194 const $item = $(this);
1195 const itemContent = $item.find('.item-content');
1196 if (itemContent.is(':visible')) {
1197 allCollapsed = false;
1198 return false; // Break the loop if we find an expanded item
1199 }
1200 });
1201
1202 if (allCollapsed) {
1203 // If all items are collapsed, expand them
1204 $button.html('<i class="fas fa-chevron-down mr-1"></i> <?php _e('Collapse All', 'easy-invoice'); ?>');
1205 $('.quote-item').each(function() {
1206 const $item = $(this);
1207 const itemContent = $item.find('.item-content');
1208 const summaryElement = $item.find('.item-collapsed-summary');
1209 const sampleButton = $item.find('.fill-sample-data-btn');
1210 const icon = $item.find('.item-collapse-toggle i');
1211
1212 // Only toggle if it's currently collapsed
1213 if (!itemContent.is(':visible')) {
1214 itemContent.slideDown(200);
1215 summaryElement.slideUp(200);
1216 sampleButton.show(); // Show sample button when expanded
1217 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
1218 // Remove collapsed item styling
1219 $item.removeClass('collapsed-item');
1220 }
1221 });
1222 } else {
1223 // If any items are expanded, collapse them all
1224 $button.html('<i class="fas fa-chevron-right mr-1"></i> <?php _e('Expand All', 'easy-invoice'); ?>');
1225 $('.quote-item').each(function() {
1226 const $item = $(this);
1227 const itemContent = $item.find('.item-content');
1228 const summaryElement = $item.find('.item-collapsed-summary');
1229 const sampleButton = $item.find('.fill-sample-data-btn');
1230 const icon = $item.find('.item-collapse-toggle i');
1231
1232 // Only toggle if it's currently expanded
1233 if (itemContent.is(':visible')) {
1234 // Update the summary before collapsing
1235 updateQuoteItemSummary($item);
1236 itemContent.slideUp(200);
1237 summaryElement.slideDown(200);
1238 sampleButton.hide(); // Hide sample button when collapsed
1239 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
1240 // Add collapsed item styling
1241 $item.addClass('collapsed-item');
1242 }
1243 });
1244 }
1245
1246 // Update totals if payment manager is available
1247 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1248 window.EasyInvoicePayment.updateTotals();
1249 } else if (typeof updatePreview === 'function') {
1250 updatePreview();
1251 }
1252 });
1253
1254 // Client search functionality
1255 let searchTimeout;
1256 let isSearching = false;
1257 $('#client-search-input').on('input', function() {
1258 const query = $(this).val().trim();
1259 clearTimeout(searchTimeout);
1260
1261 // Don't hide results immediately for short queries - let the search function handle it
1262 if (query.length === 0) {
1263 // Don't call searchClients here to avoid duplicate calls with click event
1264 return;
1265 }
1266
1267 searchTimeout = setTimeout(function() {
1268 searchClients(query);
1269 }, 300);
1270 });
1271
1272 // Focus on search input - ensure client tab is active
1273 $('#client-search-input').on('focus', function() {
1274 // Switch to client tab first to ensure the search results are visible
1275 if (window.switchToTab) {
1276 window.switchToTab('client-tab');
1277 } else {
1278 // Fallback: manually switch tabs
1279 $('.tab-button').removeClass('border-indigo-500 text-indigo-600').addClass('border-transparent text-gray-500');
1280 $('.tab-content').addClass('hidden');
1281 $('.tab-button[data-tab="client-tab"]').addClass('border-indigo-500 text-indigo-600');
1282 $('#client-tab').removeClass('hidden');
1283 }
1284 });
1285
1286 // Click on search input to show all clients
1287 $("#client-search-input").on("click", function() {
1288
1289
1290 // Switch to client tab first to ensure the search results are visible
1291 if (window.switchToTab) {
1292 window.switchToTab('client-tab');
1293 } else {
1294 // Fallback: manually switch tabs
1295 $('.tab-button').removeClass('border-indigo-500 text-indigo-600').addClass('border-transparent text-gray-500');
1296 $('.tab-content').addClass('hidden');
1297 $('.tab-button[data-tab="client-tab"]').addClass('border-indigo-500 text-indigo-600');
1298 $('#client-tab').removeClass('hidden');
1299 }
1300
1301 // Always show all clients when clicking the search input
1302 searchClients('');
1303 });
1304
1305 // Client selection
1306 $(document).on('click', '.client-option', function(e) {
1307 e.preventDefault();
1308 e.stopPropagation();
1309
1310 const clientId = $(this).data('client-id');
1311 const clientName = $(this).data('client-name');
1312 const clientEmail = $(this).data('client-email');
1313 const clientCompany = $(this).data('client-company');
1314 const clientPhone = $(this).data('client-phone');
1315 const clientWebsite = $(this).data('client-website');
1316 const clientAddress = $(this).data('client-address');
1317
1318 // Create client data object with all available information
1319 const clientData = {
1320 name: clientName,
1321 email: clientEmail,
1322 company: clientCompany,
1323 phone: clientPhone,
1324 website: clientWebsite,
1325 address: clientAddress
1326 };
1327
1328 selectClient(clientId, clientName, clientEmail, clientData);
1329 $('#client-search-results').hide();
1330 $('#client-search-input').val('');
1331 });
1332
1333 // Alternative click handler for client options
1334 $('#client-search-list').on('click', '.client-option', function(e) {
1335 e.preventDefault();
1336 e.stopPropagation();
1337
1338 const clientId = $(this).data('client-id');
1339 const clientName = $(this).data('client-name');
1340 const clientEmail = $(this).data('client-email');
1341 const clientCompany = $(this).data('client-company');
1342 const clientPhone = $(this).data('client-phone');
1343 const clientWebsite = $(this).data('client-website');
1344 const clientAddress = $(this).data('client-address');
1345
1346 // Create client data object with all available information
1347 const clientData = {
1348 name: clientName,
1349 email: clientEmail,
1350 company: clientCompany,
1351 phone: clientPhone,
1352 website: clientWebsite,
1353 address: clientAddress
1354 };
1355
1356 selectClient(clientId, clientName, clientEmail, clientData);
1357 $('#client-search-results').hide();
1358 $('#client-search-input').val('');
1359 });
1360
1361 // Clear client selection
1362 $('#clear-client-selection').on('click', function() {
1363 clearClientSelection();
1364 });
1365
1366 // Add new client button - handled by client-manager.js
1367 // $('#add-new-client-btn').on('click', function() {
1368 // $('#add_client_modal').show();
1369 // });
1370
1371 // Close modal - handled by client-manager.js
1372 // $('.close-modal').on('click', function() {
1373 // $('#add_client_modal').hide();
1374 // });
1375
1376 // Functions
1377 function addNewQuoteItem() {
1378 const template = $('#quote-item-template').html();
1379 const newItem = $(template);
1380
1381 // Update field names with new index using the same pattern as invoice builder
1382 newItem.find('input[name*="items["]').each(function() {
1383 var oldName = $(this).attr('name');
1384 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
1385 $(this).attr('name', newName);
1386 });
1387
1388 newItem.find('textarea[name*="items["]').each(function() {
1389 var oldName = $(this).attr('name');
1390 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
1391 $(this).attr('name', newName);
1392 });
1393
1394 newItem.find('select[name*="items["]').each(function() {
1395 var oldName = $(this).attr('name');
1396 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
1397 $(this).attr('name', newName);
1398 });
1399
1400 // Update field IDs to be unique (handle both _-1 and _0)
1401 newItem.find('[id*="_-1"], [id*="_0"]').each(function() {
1402 var oldId = $(this).attr('id');
1403 var newId = oldId.replace(/_(?:-1|0)/, '_' + itemIndex);
1404 $(this).attr('id', newId);
1405 // Update corresponding label for attribute
1406 var $label = newItem.find('label[for="' + oldId + '"]');
1407 if ($label.length) {
1408 $label.attr('for', newId);
1409 }
1410 });
1411
1412 $('.quote-items-container').append(newItem);
1413 itemIndex++;
1414 updateItemNumbers();
1415 }
1416
1417 function updateItemNumbers() {
1418 $('.quote-item').each(function(index) {
1419 const title = $(this).find('h3');
1420 if (title.text().includes('Item #')) {
1421 title.text('Item #' + (index + 1));
1422 }
1423 });
1424 }
1425
1426 function fillQuoteItemWithSampleData(itemContainer) {
1427 const samples = [
1428 {
1429 'title': 'Website Design',
1430 'description': 'Complete design and layout for a responsive business website.',
1431 'quantity': 1,
1432 'price': 1200.00,
1433 'taxable': true
1434 },
1435 {
1436 'title': 'Consultation Session',
1437 'description': 'One-hour business strategy consultation.',
1438 'quantity': 2,
1439 'price': 150.00,
1440 'taxable': true
1441 },
1442 {
1443 'title': 'Logo Design',
1444 'description': 'Custom logo design with 3 initial concepts and 2 revisions.',
1445 'quantity': 1,
1446 'price': 350.00,
1447 'taxable': true
1448 },
1449 {
1450 'title': 'SEO Optimization',
1451 'description': 'On-page and off-page SEO optimization for better search rankings.',
1452 'quantity': 1,
1453 'price': 500.00,
1454 'taxable': true
1455 },
1456 {
1457 'title': 'Content Writing',
1458 'description': 'Professional content writing for 5 web pages.',
1459 'quantity': 5,
1460 'price': 80.00,
1461 'taxable': true
1462 },
1463 {
1464 'title': 'Annual Hosting',
1465 'description': 'One year of premium website hosting.',
1466 'quantity': 1,
1467 'price': 200.00,
1468 'taxable': false
1469 }
1470 ];
1471 const sampleData = samples[Math.floor(Math.random() * samples.length)];
1472
1473 itemContainer.find('input[name*="[title]"]').val(sampleData.title);
1474 itemContainer.find('textarea[name*="[description]"]').val(sampleData.description);
1475 itemContainer.find('input[name*="[quantity]"]').val(sampleData.quantity);
1476 itemContainer.find('input[name*="[price]"]').val(sampleData.price);
1477 itemContainer.find('input[name*="[taxable]"]').prop('checked', sampleData.taxable);
1478
1479 // Update the item title in the header
1480 itemContainer.find('h3').text(sampleData.title);
1481
1482 // Trigger calculations
1483 calculateTotals();
1484 }
1485
1486 // Update the collapsed summary of a quote item
1487 function updateQuoteItemSummary(itemContainer) {
1488 const quantity = parseFloat(itemContainer.find('input[name*="[quantity]"]').val()) || 0;
1489 const price = parseFloat(itemContainer.find('input[name*="[price]"]').val()) || 0;
1490 const total = quantity * price;
1491 itemContainer.find('.quantity-summary').text(quantity);
1492 itemContainer.find('.price-summary').text(price.toFixed(2));
1493 itemContainer.find('.total-summary').text(total.toFixed(2));
1494 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
1495 }
1496
1497 function searchClients(query) {
1498
1499
1500 // Prevent multiple simultaneous requests
1501 if (isSearching) {
1502 return;
1503 }
1504
1505 // Clear any existing timeout to prevent multiple requests
1506 if (searchTimeout) {
1507 clearTimeout(searchTimeout);
1508 }
1509
1510 isSearching = true;
1511
1512 $('#client-search-loading').show().removeClass('hidden');
1513 $('#client-search-empty').hide().addClass('hidden');
1514 $('#client-search-list').empty();
1515
1516 // If query is empty or less than 2 characters, show all clients
1517 if (!query || query.trim().length < 2) {
1518 query = ''; // Set to empty string to get all clients
1519 }
1520
1521
1522
1523 $.ajax({
1524 url: easyInvoice.ajaxUrl,
1525 type: 'POST',
1526 data: {
1527 action: 'easy_invoice_search_clients',
1528 query: query,
1529 nonce: easyInvoice.nonce
1530 },
1531
1532 success: function(response) {
1533
1534 isSearching = false;
1535 $('#client-search-loading').hide().addClass('hidden');
1536
1537 if (response.success && response.data.length > 0) {
1538
1539 response.data.forEach(function(client) {
1540 const option = $(`
1541 <div class="client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
1542 data-client-id="${client.id}"
1543 data-client-name="${(client.name || '').replace(/"/g, '&quot;')}"
1544 data-client-email="${(client.email || '').replace(/"/g, '&quot;')}"
1545 data-client-company="${(client.company || '').replace(/"/g, '&quot;')}"
1546 data-client-phone="${(client.phone || '').replace(/"/g, '&quot;')}"
1547 data-client-address="${(client.address || '').replace(/"/g, '&quot;')}"
1548 onclick="selectClientFromOption(this)">
1549 <div class="font-medium text-gray-900">${client.name || 'No Name'}</div>
1550 <div class="text-sm text-gray-600">${client.email || 'No Email'}</div>
1551 </div>
1552 `);
1553 $('#client-search-list').append(option);
1554 });
1555 $('#client-search-results').show().removeClass('hidden');
1556
1557 } else {
1558 $('#client-search-empty').show().removeClass('hidden');
1559 $('#client-search-results').show().removeClass('hidden');
1560 }
1561 },
1562 error: function(xhr, status, error) {
1563 console.error('Quote form: AJAX error:', error);
1564 console.error('Quote form: Status:', status);
1565 console.error('Quote form: Response:', xhr.responseText);
1566 isSearching = false;
1567 $('#client-search-loading').hide().addClass('hidden');
1568 $('#client-search-empty').show().removeClass('hidden');
1569 $('#client-search-results').show().removeClass('hidden');
1570 }
1571 });
1572 }
1573
1574 function selectClient(clientId, clientName, clientEmail, clientData = null) {
1575 $('#select-client').val(clientId);
1576 $('#client-id').val(clientId);
1577 $('#selected-client-name').text(clientName);
1578 $('#selected-client-email').text(clientEmail);
1579
1580 $('#selected-client-display').show().removeClass('hidden');
1581 $('#client-info-display').show().removeClass('hidden');
1582 $('#no-client-message').hide().addClass('hidden');
1583
1584 // Update the edit client button URL
1585 $('#edit-selected-client').attr('href', '<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id='); ?>' + clientId);
1586
1587 // If we have full client data, populate all fields directly
1588 if (clientData) {
1589 $('#display-client-name').text(clientData.name || clientName || '-');
1590 $('#display-client-company').text(clientData.company || '-');
1591 $('#display-client-email').text(clientData.email || clientEmail || '-');
1592 $('#display-client-phone').text(clientData.phone || '-');
1593 $('#display-client-website').text(clientData.website || '-');
1594 $('#display-client-address').text(clientData.address || '-');
1595 } else {
1596 // Fallback to just the basic info we have
1597 $('#display-client-name').text(clientName);
1598 $('#display-client-email').text(clientEmail);
1599 $('#display-client-company').text('-');
1600 $('#display-client-phone').text('-');
1601 $('#display-client-website').text('-');
1602 $('#display-client-address').text('-');
1603 }
1604 }
1605
1606 function clearClientSelection() {
1607 $('#select-client').val('');
1608 $('#client-id').val('');
1609 $('#selected-client-display').hide();
1610 $('#client-info-display').hide();
1611 $('#no-client-message').show();
1612
1613 // Clear display fields
1614 $('#display-client-name, #display-client-email, #display-client-company, #display-client-phone, #display-client-website, #display-client-address').text('-');
1615 }
1616
1617 function calculateTotals() {
1618 let subtotal = 0;
1619 let taxableSubtotal = 0;
1620 $('.quote-item').each(function() {
1621 const quantity = parseFloat($(this).find('input[name*="[quantity]"]').val()) || 0;
1622 const price = parseFloat($(this).find('input[name*="[price]"]').val()) || 0;
1623 let adjustPercentage = 0;
1624 // Only apply adjust percentage if the adjust field is enabled
1625 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
1626 adjustPercentage = parseFloat($(this).find('input[name*="[adjust_percentage]"]').val()) || 0;
1627 }
1628 const isTaxable = $(this).find('input[name*="[taxable]"]').is(':checked');
1629 const baseTotal = quantity * price;
1630 const total = baseTotal * (1 + adjustPercentage / 100);
1631 $(this).find('.total-summary').text(total.toFixed(2));
1632 $(this).find('input[name*="[total]"]').val(total.toFixed(2));
1633 subtotal += total;
1634 if (isTaxable) {
1635 taxableSubtotal += total;
1636 }
1637 });
1638 $('input[name="subtotal"]').val(subtotal.toFixed(2));
1639 calculateTaxAndDiscount(subtotal, taxableSubtotal);
1640 }
1641
1642 function calculateTaxAndDiscount(subtotal, taxableSubtotal) {
1643 subtotal = (subtotal !== undefined) ? subtotal : (parseFloat($('input[name="subtotal"]').val()) || 0);
1644 taxableSubtotal = (taxableSubtotal !== undefined) ? taxableSubtotal : subtotal;
1645
1646 const discountValue = parseFloat($('input[name="discount_value"]').val()) || 0;
1647 const discountType = $('select[name="discount_type"]').val();
1648 const calculationMethod = $('select[name="discount_calculation_method"]').val() || 'before_tax';
1649 const taxRate = parseFloat($('input[name="tax_rate"]').val()) || 0;
1650 const pricesIncludeTax = $('select[name="prices_include_tax"]').val() === 'yes';
1651
1652 // Calculate discount
1653 let discountAmount = 0;
1654 if (discountType === 'percentage') {
1655 discountAmount = subtotal * (discountValue / 100);
1656 } else if (discountType === 'fixed') {
1657 discountAmount = Math.min(discountValue, subtotal);
1658 }
1659 // discountType === 'none': discountAmount stays 0
1660
1661 const afterDiscountAmount = subtotal - discountAmount;
1662 const discountRatio = (subtotal > 0) ? (discountAmount / subtotal) : 0;
1663
1664 // Calculate tax
1665 let taxAmount = 0;
1666 let total = 0;
1667
1668 if (pricesIncludeTax && taxRate > 0) {
1669 // Extract tax from taxable portion (reverse calculation)
1670 const taxableAfterDiscount = taxableSubtotal * (1 - discountRatio);
1671 taxAmount = taxableAfterDiscount - (taxableAfterDiscount / (1 + taxRate / 100));
1672 total = afterDiscountAmount;
1673 } else if (taxRate > 0) {
1674 if (calculationMethod === 'before_tax') {
1675 // Discount first, then tax on remaining taxable amount
1676 const taxableAfterDiscount = taxableSubtotal * (1 - discountRatio);
1677 taxAmount = taxableAfterDiscount * (taxRate / 100);
1678 } else {
1679 // after_tax: Tax first on full taxable subtotal, then discount
1680 taxAmount = taxableSubtotal * (taxRate / 100);
1681 if (discountType === 'percentage') {
1682 const totalBeforeDiscount = subtotal + taxAmount;
1683 discountAmount = totalBeforeDiscount * (discountValue / 100);
1684 }
1685 }
1686 total = (subtotal - discountAmount) + taxAmount;
1687 } else {
1688 total = afterDiscountAmount;
1689 }
1690
1691 $('input[name="discount_amount"]').val(discountAmount.toFixed(2));
1692 $('input[name="tax_amount"]').val(taxAmount.toFixed(2));
1693 $('input[name="total"]').val(total.toFixed(2));
1694 }
1695
1696 // Initialize calculations on page load
1697 calculateTotals();
1698
1699 // Initialize existing client data on page load
1700 const existingClientId = $('#client-id').val();
1701 const existingClientName = $('#selected-client-name').text().trim();
1702 const existingClientEmail = $('#selected-client-email').text().trim();
1703
1704 // Get existing client data from PHP
1705 const existingClientData = <?php echo json_encode($client_data); ?>;
1706
1707 if (existingClientId && existingClientName && existingClientName !== '-') {
1708 // Show the selected client display and client info display
1709 $('#selected-client-display').show();
1710 $('#client-info-display').show();
1711 $('#no-client-message').hide();
1712
1713 // Set the select client dropdown value without triggering change event
1714 // Use a flag to prevent AJAX calls during initialization
1715 window.isInitializingClient = true;
1716 $('#select-client').val(existingClientId);
1717 setTimeout(function() {
1718 window.isInitializingClient = false;
1719 }, 1000);
1720
1721 // Populate the search input with the existing client name
1722 $('#client-search-input').val(existingClientName);
1723
1724 // Client initialized successfully
1725 }
1726
1727 // Bind calculation events
1728 $(document).on('input', 'input[name*="[quantity]"], input[name*="[price]"], input[name*="[adjust_percentage]"]', function() {
1729 calculateTotals();
1730 });
1731
1732 $(document).on('change', 'input[name*="[taxable]"]', function() {
1733 calculateTotals();
1734 });
1735
1736 $(document).on('input', 'input[name="discount_value"], input[name="tax_rate"]', function() {
1737 calculateTotals();
1738 });
1739
1740 $(document).on('change', 'select[name="discount_type"], select[name="discount_calculation_method"], select[name="prices_include_tax"]', function() {
1741 calculateTotals();
1742 });
1743
1744 // Click on search input to show all clients
1745 $("#client-search-input").on("click", function() {
1746 // Only show all clients if the search results are not already visible
1747 if (!$('#client-search-results').is(':visible')) {
1748 searchClients('');
1749 }
1750 });
1751
1752 // Global function for inline onclick
1753 window.selectClientFromOption = function(element) {
1754 const $element = $(element);
1755 const clientId = $element.data('client-id');
1756 const clientName = $element.data('client-name');
1757 const clientEmail = $element.data('client-email');
1758 const clientCompany = $element.data('client-company');
1759 const clientPhone = $element.data('client-phone');
1760 const clientWebsite = $element.data('client-website');
1761 const clientAddress = $element.data('client-address');
1762
1763 // Create client data object with all available information
1764 const clientData = {
1765 name: clientName,
1766 email: clientEmail,
1767 company: clientCompany,
1768 phone: clientPhone,
1769 website: clientWebsite,
1770 address: clientAddress
1771 };
1772
1773 selectClient(clientId, clientName, clientEmail, clientData);
1774 $('#client-search-results').hide();
1775 $('#client-search-input').val('');
1776 };
1777 });
1778
1779 // Quote field configuration and pre-loaded data
1780 </script>
1781
1782 <?php if (wp_doing_ajax()): ?>
1783 <div class="mt-8 pt-6 border-t border-gray-200">
1784 <div class="flex justify-end space-x-3">
1785 <button type="button" id="cancel-quote-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">
1786 Cancel
1787 </button>
1788 <button type="submit" id="save-quote-btn" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
1789 <i class="fas fa-save mr-2"></i>
1790 <span>Save Quote</span>
1791 </button>
1792 </div>
1793 </div>
1794 </form>
1795 <?php endif; ?>