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

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