PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.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 / clients-page.php

clients-page.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at templates/clients-page.php

625 lines 38.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5 // Extract data passed from controller
6 $clients = $clients ?? [];
7 $stats = $stats ?? [];
8 $current_page = $current_page ?? 1;
9 $per_page = $per_page ?? 20;
10 $total_clients = $total_clients ?? 0;
11 $total_pages = $total_pages ?? 1;
12
13 // $clients variable contains Client objects
14
15 ?>
16 <style>
17 <?php if (!easy_invoice_user_can('ei_manage_clients')) : ?>.client-list-table a[href*="client-edit"], .client-list-table .delete-client { display: none; }<?php endif; ?>
18 /* Eight columns do not fit a laptop; the username and role are the least useful. */
19 .client-list-table th, .client-list-table td { padding-left: 0.875rem; padding-right: 0.875rem; }
20 @media (max-width: 1700px) {
21 .client-list-table th:nth-child(6), .client-list-table td:nth-child(6),
22 .client-list-table th:nth-child(7), .client-list-table td:nth-child(7) { display: none; }
23 }
24 </style>
25 <div class="p-8">
26 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6" style="margin-left: -2rem; margin-top: -2rem; padding-left:2rem; padding-right:2rem; margin-right: -2rem;">
27 <div class="flex items-center justify-between">
28 <div>
29 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Clients', 'easy-invoice'); ?></h1>
30 <p class="mt-1 text-sm text-gray-500"><?php esc_html_e('Manage all client records', 'easy-invoice'); ?></p>
31 </div>
32 <div class="flex items-center space-x-3">
33 <?php
34 // "Export All" is part of the bulk_operations addon
35 // (Personal tier). See templates/invoices/listing.php
36 // for the canonical three-state gate pattern.
37 $ei_bulk_ops_loaded = \EasyInvoice\Addons\AddonManager::shouldLoad('bulk_operations');
38 ?>
39 <?php if ($ei_bulk_ops_loaded): ?>
40 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
41 <input type="hidden" name="action" value="easy_invoice_export_all_clients" />
42 <input type="hidden" name="_wpnonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_export_all_clients')); ?>" />
43 <button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
44 <svg class="w-4 h-4 mr-2 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
45 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
46 </svg>
47 <?php esc_html_e('Export All', 'easy-invoice'); ?>
48 </button>
49 </form>
50 <?php elseif (!easy_invoice_has_pro()): ?>
51 <button type="button" id="export-all-clients-btn" class="premium-export-btn inline-flex items-center px-4 py-2 border border-gray-300 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 transition-colors duration-200 relative">
52 <svg class="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
53 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
54 </svg>
55 <?php esc_html_e('Export All', 'easy-invoice'); ?>
56 <svg class="ml-2 w-5 h-5 text-purple-600 crown-icon" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
57 <path d="M12 8L15 13.2L18 10.5L17.3 14H6.7L6 10.5L9 13.2L12 8M12 4L8.5 10L3 5L5 16H19L21 5L15.5 10L12 4Z"/>
58 </svg>
59 </button>
60 <script>
61 jQuery(document).ready(function($) {
62 $('#export-all-clients-btn').on('click', function(e) {
63 e.preventDefault();
64 if (typeof EasyInvoiceConfirmation !== 'undefined') {
65 EasyInvoiceConfirmation.showFeatureUpgrade('Export All Clients', 'Export all your clients to CSV, Excel, or PDF format for backup, analysis, or sharing with your team.');
66 } else {
67 EasyInvoiceToast.error('<?php echo esc_js(__('Exporting all clients is a premium feature. Upgrade to Easy Invoice Pro to unlock this feature.', 'easy-invoice')); ?>');
68 }
69 });
70 });
71 </script>
72 <?php endif; ?>
73 <?php if (easy_invoice_user_can('ei_manage_clients')) : ?>
74 <button id="add-new-client-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">
75 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
76 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
77 </svg>
78 <?php esc_html_e('Add New Client', 'easy-invoice'); ?>
79 </button>
80 <?php endif; ?>
81 </div>
82 </div>
83 </div>
84
85 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 mt-8">
86 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
87 <div class="flex items-center">
88 <div class="rounded-full bg-blue-100 p-3 mr-4">
89 <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
90 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 21V19C17 17.9391 16.5786 16.9217 15.8284 16.1716C15.0783 15.4214 14.0609 15 13 15H5C3.93913 15 2.92172 15.4214 2.17157 16.1716C1.42143 16.9217 1 17.9391 1 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
91 <circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
92 <path d="M23 21V19C23 18.1137 22.6485 17.2628 22.0237 16.6425C21.3989 16.0221 20.5471 15.6722 19.66 15.6722C18.7729 15.6722 17.9211 16.0221 17.2963 16.6425C16.6715 17.2628 16.32 18.1137 16.32 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
93 <path d="M16.32 11.6722C18.36 11.6722 20 10.0322 20 7.99219C20 5.95219 18.36 4.31219 16.32 4.31219" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
94 </svg>
95 </div>
96 <div>
97 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Clients', 'easy-invoice'); ?></h3>
98 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo (int) (isset($stats['total_clients']) ? $stats['total_clients'] : count($clients)); ?></p>
99 </div>
100 </div>
101 </div>
102 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
103 <div class="flex items-center">
104 <div class="rounded-full bg-indigo-100 p-3 mr-4">
105 <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
106 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
107 </svg>
108 </div>
109 <div>
110 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Invoices', 'easy-invoice'); ?></h3>
111 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo esc_html(isset($stats['total_invoices']) ? $stats['total_invoices'] : 0); ?></p>
112 </div>
113 </div>
114 </div>
115 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
116 <div class="flex items-center">
117 <div class="rounded-full bg-green-100 p-3 mr-4">
118 <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
119 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
120 </svg>
121 </div>
122 <div>
123 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Revenue', 'easy-invoice'); ?></h3>
124 <?php if (is_array($stats['total_revenue']) && !empty($stats['total_revenue'])): ?>
125 <div class="mt-1">
126 <?php
127 $currency_count = count($stats['total_revenue']);
128 foreach ($stats['total_revenue'] as $index => $data):
129 $is_last = $index === $currency_count - 1;
130 // Get currency code from the data array or use a fallback
131 $currency_code = isset($data['currency_code']) ? $data['currency_code'] : 'USD';
132 ?>
133 <div class="<?php echo $currency_count > 1 ? 'text-sm' : 'text-lg'; ?> font-bold text-gray-900 <?php echo !$is_last ? 'mb-1' : ''; ?>">
134 <?php echo esc_html($data['symbol'] . number_format($data['amount'], 2)); ?>
135 <span class="text-xs font-normal text-gray-500"><?php echo esc_html(strtoupper($currency_code ?? 'USD')); ?></span>
136 </div>
137 <?php endforeach; ?>
138 </div>
139 <?php else: ?>
140 <p class="mt-1 text-lg font-bold text-gray-900">$0.00</p>
141 <?php endif; ?>
142 </div>
143 </div>
144 </div>
145 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
146 <div class="flex items-center">
147 <div class="rounded-full bg-yellow-100 p-3 mr-4">
148 <svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
149 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
150 </svg>
151 </div>
152 <div>
153 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Pending Invoices', 'easy-invoice'); ?></h3>
154 <p class="mt-1 text-2xl font-bold text-yellow-600"><?php echo esc_html(isset($stats['pending_invoices']) ? $stats['pending_invoices'] : 0); ?></p>
155 </div>
156 </div>
157 </div>
158 </div>
159
160 <?php if ($total_clients > 0): ?>
161 <div class="mb-4">
162 <div class="px-4 py-3 flex items-center justify-between">
163 <div class="flex items-center space-x-2">
164 <span class="text-sm text-gray-700">
165 Showing
166 <span class="font-medium"><?php echo number_format((($current_page - 1) * $per_page) + 1); ?></span>
167 to
168 <span class="font-medium"><?php echo number_format(min($current_page * $per_page, $total_clients)); ?></span>
169 of
170 <span class="font-medium"><?php echo number_format($total_clients); ?></span>
171 results
172 </span>
173 </div>
174 <?php if ($total_pages > 1): ?>
175 <div class="flex items-center space-x-2">
176 <span class="text-sm text-gray-500">Page <?php echo number_format($current_page); ?> of <?php echo number_format($total_pages); ?></span>
177 <?php
178 // Use WordPress's built-in pagination for top
179 $top_pagination_args = array(
180 'base' => add_query_arg('paged', '%#%'),
181 'format' => '',
182 'current' => $current_page,
183 'total' => $total_pages,
184 'prev_text' => '‹ Previous',
185 'next_text' => 'Next ›',
186 'type' => 'array',
187 'end_size' => 1,
188 'mid_size' => 2,
189 'add_args' => array(
190 'page' => 'easy-invoice-clients'
191 )
192 );
193
194 $top_pagination_links = paginate_links($top_pagination_args);
195
196 if ($top_pagination_links) {
197 echo '<div class="flex space-x-1">';
198 foreach ($top_pagination_links as $link) {
199 // Add null check to prevent passing null to easy_invoice_str_replace
200 if ($link === null) {
201 continue;
202 }
203
204 // Convert WordPress pagination to our styling
205 $link = easy_invoice_str_replace('page-numbers', 'px-3 py-2 text-sm font-medium border border-gray-300 rounded-md', $link);
206 $link = easy_invoice_str_replace('current', 'bg-indigo-50 border-indigo-500 text-indigo-600', $link);
207 $link = easy_invoice_str_replace('prev', 'bg-white text-gray-500 hover:bg-gray-50', $link);
208 $link = easy_invoice_str_replace('next', 'bg-white text-gray-500 hover:bg-gray-50', $link);
209 $link = easy_invoice_str_replace('dots', 'bg-white text-gray-700', $link);
210
211 // Add default styling for regular page numbers
212 if ($link && strpos($link, 'bg-indigo-50') === false && strpos($link, 'bg-white') === false) {
213 $link = easy_invoice_str_replace('border-gray-300', 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50', $link);
214 }
215
216 echo wp_kses_post($link);
217 }
218 echo '</div>';
219 }
220 ?>
221 </div>
222 <?php endif; ?>
223 </div>
224 </div>
225 <?php endif; ?>
226
227 <div class="bg-white rounded-lg shadow">
228 <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between gap-3 flex-wrap">
229 <label for="client-search" class="sr-only"><?php esc_html_e('Search clients', 'easy-invoice'); ?></label>
230 <div class="relative w-full sm:w-80">
231 <svg class="absolute left-3 top-2.5 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M17 11A6 6 0 115 11a6 6 0 0112 0z"/></svg>
232 <input type="search" id="client-search" placeholder="<?php esc_attr_e('Search by name, company, email or phone…', 'easy-invoice'); ?>" class="block w-full pl-9 pr-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-indigo-500 focus:border-indigo-500">
233 </div>
234 <span id="client-search-count" class="text-sm text-gray-500"></span>
235 </div>
236 <div class="overflow-x-auto">
237 <table class="min-w-full divide-y divide-gray-200 client-list-table">
238 <thead class="bg-gray-50">
239 <tr>
240 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="id">
241 <div class="flex items-center space-x-1">
242 <span><?php esc_html_e('Client ID', 'easy-invoice'); ?></span>
243 <div class="sort-indicator">
244 <i class="fas fa-sort text-gray-400"></i>
245 </div>
246 </div>
247 </th>
248 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="business_name">
249 <div class="flex items-center space-x-1">
250 <span><?php esc_html_e('Business Name', 'easy-invoice'); ?></span>
251 <div class="sort-indicator">
252 <i class="fas fa-sort text-gray-400"></i>
253 </div>
254 </div>
255 </th>
256 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="name">
257 <div class="flex items-center space-x-1">
258 <span><?php esc_html_e('Client Name', 'easy-invoice'); ?></span>
259 <div class="sort-indicator">
260 <i class="fas fa-sort text-gray-400"></i>
261 </div>
262 </div>
263 </th>
264 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="email">
265 <div class="flex items-center space-x-1">
266 <span><?php esc_html_e('Email', 'easy-invoice'); ?></span>
267 <div class="sort-indicator">
268 <i class="fas fa-sort text-gray-400"></i>
269 </div>
270 </div>
271 </th>
272 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="phone">
273 <div class="flex items-center space-x-1">
274 <span><?php esc_html_e('Phone', 'easy-invoice'); ?></span>
275 <div class="sort-indicator">
276 <i class="fas fa-sort text-gray-400"></i>
277 </div>
278 </div>
279 </th>
280 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="username">
281 <div class="flex items-center space-x-1">
282 <span><?php esc_html_e('Username', 'easy-invoice'); ?></span>
283 <div class="sort-indicator">
284 <i class="fas fa-sort text-gray-400"></i>
285 </div>
286 </div>
287 </th>
288 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="role">
289 <div class="flex items-center space-x-1">
290 <span><?php esc_html_e('Role', 'easy-invoice'); ?></span>
291 <div class="sort-indicator">
292 <i class="fas fa-sort text-gray-400"></i>
293 </div>
294 </div>
295 </th>
296 <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
297 Actions
298 </th>
299 </tr>
300 </thead>
301 <tbody class="bg-white divide-y divide-gray-200">
302 <?php if (empty($clients)): ?>
303 <tr>
304 <td colspan="8" class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500">
305 No clients found
306 </td>
307 </tr>
308 <?php endif; ?>
309 <?php foreach ($clients as $client): ?>
310 <?php
311 // Get business name separately
312 $businessName = $client->getBusinessClientName();
313
314 // Get contact name (first + last name)
315 $contactName = trim($client->getFirstName() . ' ' . $client->getLastName());
316 if(empty($contactName)) {
317 $contactName = $client->getUsername();
318 }
319
320 $clientId = $client->getId();
321
322 // Get client role
323 $user = get_user_by('ID', $clientId);
324 $client_role = $user ? $user->roles[0] ?? 'client' : 'client';
325
326 // Role styling
327 $role_class = 'bg-blue-100 text-blue-800';
328 $role_icon = 'fas fa-user';
329
330 switch ($client_role) {
331 case 'administrator':
332 $role_class = 'bg-red-100 text-red-800';
333 $role_icon = 'fas fa-crown';
334 break;
335 case 'editor':
336 $role_class = 'bg-purple-100 text-purple-800';
337 $role_icon = 'fas fa-user-edit';
338 break;
339 case 'author':
340 $role_class = 'bg-green-100 text-green-800';
341 $role_icon = 'fas fa-user-edit';
342 break;
343 case 'contributor':
344 $role_class = 'bg-yellow-100 text-yellow-800';
345 $role_icon = 'fas fa-user-plus';
346 break;
347 case 'subscriber':
348 $role_class = 'bg-gray-100 text-gray-800';
349 $role_icon = 'fas fa-user';
350 break;
351 default:
352 $role_class = 'bg-blue-100 text-blue-800';
353 $role_icon = 'fas fa-user';
354 }
355 ?>
356 <tr>
357 <td class="px-6 py-4 whitespace-nowrap">
358 <div class="text-sm text-gray-500 font-mono font-semibold"><?php echo esc_html($clientId); ?></div>
359 </td>
360 <td class="px-6 py-4 whitespace-nowrap">
361 <div class="text-sm font-medium text-gray-900"><?php echo esc_html($businessName ?: '-'); ?></div>
362 </td>
363 <td class="px-6 py-4 whitespace-nowrap">
364 <div class="text-sm font-medium text-gray-900"><?php echo esc_html($contactName); ?></div>
365 </td>
366 <td class="px-6 py-4 whitespace-nowrap">
367 <div class="text-sm text-gray-500"><?php echo esc_html($client->getEmail()); ?></div>
368 </td>
369 <td class="px-6 py-4 whitespace-nowrap">
370 <div class="text-sm text-gray-500"><?php echo esc_html($client->getPhone()); ?></div>
371 </td>
372 <td class="px-6 py-4 whitespace-nowrap">
373 <div class="text-sm text-gray-500"><?php echo esc_html($client->getUsername()); ?></div>
374 </td>
375 <td class="px-6 py-4 whitespace-nowrap">
376 <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <?php echo esc_attr($role_class); ?>">
377 <i class="<?php echo esc_attr($role_icon); ?> mr-1"></i>
378 <?php echo esc_html(ucfirst($client_role)); ?>
379 </span>
380 </td>
381 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
382 <div class="flex items-center space-x-2">
383 <a href="?page=easy-invoice-client-view&client_id=<?php echo esc_attr($clientId); ?>"
384 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
385 title="View Client">
386 <i class="fas fa-eye mr-1"></i>
387 View
388 </a>
389 <a href="?page=easy-invoice-client-edit&client_id=<?php echo esc_attr($clientId); ?>"
390 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
391 title="Edit Client">
392 <i class="fas fa-edit mr-1"></i>
393 Edit
394 </a>
395 <a href="<?php echo esc_url(\EasyInvoice\Controllers\StatementController::pageUrl((int) $clientId)); ?>"
396 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-gray-700 bg-gray-100 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200"
397 title="<?php esc_attr_e('Statement of account', 'easy-invoice'); ?>">
398 <i class="fas fa-file-invoice-dollar mr-1"></i>
399 <?php esc_html_e('Statement', 'easy-invoice'); ?>
400 </a>
401 <?php if ($client_role === 'administrator'): ?>
402 <button type="button"
403 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-gray-400 bg-gray-100 cursor-not-allowed opacity-50"
404 title="Cannot delete administrator"
405 disabled>
406 <i class="fas fa-trash mr-1"></i>
407 Delete
408 </button>
409 <?php else: ?>
410 <button type="button"
411 class="delete-client inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
412 data-id="<?php echo esc_attr($clientId); ?>"
413 title="Delete Client">
414 <i class="fas fa-trash mr-1"></i>
415 Delete
416 </button>
417 <?php endif; ?>
418 </div>
419 </td>
420 </tr>
421 <?php endforeach; ?>
422 </tbody>
423 </table>
424 </div>
425 </div>
426
427 <?php if ($total_clients > 0): ?>
428 <div class="mt-4">
429 <div class="px-4 py-3 flex items-center justify-between">
430 <div class="flex items-center space-x-2">
431 <span class="text-sm text-gray-700">
432 Showing
433 <span class="font-medium"><?php echo number_format((($current_page - 1) * $per_page) + 1); ?></span>
434 to
435 <span class="font-medium"><?php echo number_format(min($current_page * $per_page, $total_clients)); ?></span>
436 of
437 <span class="font-medium"><?php echo number_format($total_clients); ?></span>
438 results
439 </span>
440 </div>
441 <?php if ($total_pages > 1): ?>
442 <div class="flex items-center space-x-2">
443 <span class="text-sm text-gray-500">Page <?php echo number_format($current_page); ?> of <?php echo number_format($total_pages); ?></span>
444 <?php
445 // Use WordPress's built-in pagination for bottom
446 $bottom_pagination_args = array(
447 'base' => add_query_arg('paged', '%#%'),
448 'format' => '',
449 'current' => $current_page,
450 'total' => $total_pages,
451 'prev_text' => '‹ Previous',
452 'next_text' => 'Next ›',
453 'type' => 'array',
454 'end_size' => 1,
455 'mid_size' => 2,
456 'add_args' => array(
457 'page' => 'easy-invoice-clients'
458 )
459 );
460
461 $bottom_pagination_links = paginate_links($bottom_pagination_args);
462
463 if ($bottom_pagination_links) {
464 echo '<div class="flex space-x-1">';
465 foreach ($bottom_pagination_links as $link) {
466 // Add null check to prevent passing null to easy_invoice_str_replace
467 if ($link === null) {
468 continue;
469 }
470
471 // Convert WordPress pagination to our styling
472 $link = easy_invoice_str_replace('page-numbers', 'px-3 py-2 text-sm font-medium border border-gray-300 rounded-md', $link);
473 $link = easy_invoice_str_replace('current', 'bg-indigo-50 border-indigo-500 text-indigo-600', $link);
474 $link = easy_invoice_str_replace('prev', 'bg-white text-gray-500 hover:bg-gray-50', $link);
475 $link = easy_invoice_str_replace('next', 'bg-white text-gray-500 hover:bg-gray-50', $link);
476 $link = easy_invoice_str_replace('dots', 'bg-white text-gray-700', $link);
477
478 // Add default styling for regular page numbers
479 if ($link && strpos($link, 'bg-indigo-50') === false && strpos($link, 'bg-white') === false) {
480 $link = easy_invoice_str_replace('border-gray-300', 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50', $link);
481 }
482
483 echo wp_kses_post($link);
484 }
485 echo '</div>';
486 }
487 ?>
488 </div>
489 <?php endif; ?>
490 </div>
491 </div>
492 <?php endif; ?>
493
494 <div id="add_client_modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center hidden z-50">
495 <div class="bg-white rounded-lg shadow-xl max-w-3xl w-full p-6 relative">
496 <button class="close-modal absolute top-4 right-4 text-gray-400 hover:text-gray-500">
497 <i class="fas fa-times"></i>
498 </button>
499 <h2 class="text-xl font-medium text-gray-900 mb-4"><?php esc_html_e('Add New Client', 'easy-invoice'); ?></h2>
500
501 <?php
502 // Unset the $client variable from the foreach loop to ensure clean add form
503 unset($client);
504 include_once EASY_INVOICE_PLUGIN_DIR . 'templates/client-form.php';
505 ?>
506 </div>
507 </div>
508 </div>
509
510 <script type="text/javascript">
511 jQuery(document).ready(function($) {
512 // Client functionality is now handled by client-manager.js
513 // Only keep delete functionality here since it's specific to the clients listing page
514
515 // Table sorting functionality
516 let currentSort = { column: 'id', direction: 'desc' };
517
518 // Initialize sorting
519 function initSorting() {
520 // Set initial sort indicator
521 updateSortIndicator('id', 'desc');
522
523 // Add click handlers to sortable headers
524 $('th[data-sort]').on('click', function() {
525 const column = $(this).data('sort');
526 const currentDirection = currentSort.column === column ? currentSort.direction : 'desc';
527 const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
528
529 currentSort = { column: column, direction: newDirection };
530 updateSortIndicator(column, newDirection);
531 sortTable(column, newDirection);
532 });
533 }
534
535 // Update sort indicator
536 function updateSortIndicator(activeColumn, direction) {
537 // Reset all indicators
538 $('.sort-indicator i').removeClass('fa-sort-up fa-sort-down').addClass('fa-sort text-gray-400');
539
540 // Set active indicator
541 $(`th[data-sort="${activeColumn}"] .sort-indicator i`)
542 .removeClass('fa-sort text-gray-400')
543 .addClass(direction === 'asc' ? 'fa-sort-up text-indigo-600' : 'fa-sort-down text-indigo-600');
544 }
545
546 // Sort table function
547 function sortTable(column, direction) {
548 const tbody = $('tbody');
549 const rows = tbody.find('tr').toArray();
550
551 rows.sort(function(a, b) {
552 let aValue, bValue;
553
554 switch(column) {
555 case 'id':
556 aValue = parseInt($(a).find('td:first-child div').text());
557 bValue = parseInt($(b).find('td:first-child div').text());
558 break;
559 case 'business_name':
560 aValue = $(a).find('td:nth-child(2) div').text().toLowerCase();
561 bValue = $(b).find('td:nth-child(2) div').text().toLowerCase();
562 break;
563 case 'name':
564 aValue = $(a).find('td:nth-child(3) div').text().toLowerCase();
565 bValue = $(b).find('td:nth-child(3) div').text().toLowerCase();
566 break;
567 case 'email':
568 aValue = $(a).find('td:nth-child(4) div').text().toLowerCase();
569 bValue = $(b).find('td:nth-child(4) div').text().toLowerCase();
570 break;
571 case 'phone':
572 aValue = $(a).find('td:nth-child(5) div').text().toLowerCase();
573 bValue = $(b).find('td:nth-child(5) div').text().toLowerCase();
574 break;
575 case 'username':
576 aValue = $(a).find('td:nth-child(6) div').text().toLowerCase();
577 bValue = $(b).find('td:nth-child(6) div').text().toLowerCase();
578 break;
579 case 'role':
580 aValue = $(a).find('td:nth-child(7) span').text().toLowerCase();
581 bValue = $(b).find('td:nth-child(7) span').text().toLowerCase();
582 break;
583 default:
584 return 0;
585 }
586
587 if (direction === 'asc') {
588 return aValue > bValue ? 1 : -1;
589 } else {
590 return aValue < bValue ? 1 : -1;
591 }
592 });
593
594 // Reorder rows with animation
595 tbody.find('tr').fadeOut(200, function() {
596 tbody.empty();
597 $.each(rows, function(index, row) {
598 tbody.append(row);
599 });
600 tbody.find('tr').fadeIn(200);
601 });
602 }
603
604 // Initialize sorting
605 initSorting();
606
607 // Delete client functionality
608 // Filter the rows as you type; no request, the list is already here.
609 (function () {
610 var $input = $('#client-search'), $count = $('#client-search-count');
611 if (!$input.length) { return; }
612 $input.on('input', function () {
613 var q = $.trim($(this).val()).toLowerCase(), shown = 0, total = 0;
614 $('.client-list-table tbody tr').each(function () {
615 var $tr = $(this); if ($tr.find('td').length < 3) { return; }
616 total++;
617 var hit = !q || $tr.text().toLowerCase().indexOf(q) !== -1;
618 $tr.toggle(hit); if (hit) { shown++; }
619 });
620 $count.text(q ? shown + ' / ' + total : '');
621 });
622 })();
623 // Delete is handled by assets/js/clients.js (choice of keeping or removing the client's documents).
624 });
625 </script>