PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.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.4.1, at templates/quotes/form.php

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