Deploy: 2026-05-22 00:12:07

This commit is contained in:
Hamza-Ayed
2026-05-22 00:12:07 +03:00
parent 0afb41a3e8
commit 9450af6c8e
3 changed files with 136 additions and 1 deletions

View File

@@ -70,4 +70,47 @@ class GroupController extends BaseController
$response->json(['status' => 'success', 'message' => 'Contact added to group']);
}
/**
* Attach multiple contacts to a group in bulk
*/
public function bulkAddContacts(Request $request, Response $response)
{
$errors = $this->validate($request, [
'group_id' => 'required',
'contact_ids' => 'required'
]);
if (!empty($errors)) {
$response->status(400)->json(['status' => 'error', 'errors' => $errors]);
return;
}
$body = $request->getBody();
$groupId = (int)$body['group_id'];
$contactIds = $body['contact_ids'];
if (!is_array($contactIds)) {
$response->status(400)->json(['status' => 'error', 'message' => 'contact_ids must be an array']);
return;
}
$groupModel = new ContactGroup();
$pdo = \App\Core\Database::getConnection();
try {
$pdo->beginTransaction();
foreach ($contactIds as $contactId) {
$groupModel->attachContact($groupId, (int)$contactId);
}
$pdo->commit();
} catch (\Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$response->status(500)->json(['status' => 'error', 'message' => 'Bulk insert failed: ' . $e->getMessage()]);
return;
}
$response->json(['status' => 'success', 'message' => 'Contacts added to group in bulk']);
}
}

View File

@@ -713,7 +713,7 @@
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
<span>📱</span> WhatsApp Connection
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts()" id="nav-contacts-btn">
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
<span>👥</span> Contacts Directory
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'templates' }" @click="activeDashboardTab = 'templates'; fetchTemplates()" id="nav-templates-btn">
@@ -846,10 +846,31 @@
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="openAddContactModal()" id="btn-add-contact">+ Add Contact</button>
</div>
<!-- Bulk Action Bar -->
<div x-show="selectedContactIds.length > 0" class="bulk-action-bar" style="background: rgba(6, 182, 212, 0.1); border: 1px solid var(--primary-accent); padding: 1rem; border-radius: 10px; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; animation: fadeIn 0.3s ease-out;">
<span style="font-size: 0.9rem; color: var(--text-primary); font-weight: 600;">
Selected: <span x-text="selectedContactIds.length" style="color: var(--primary-accent);"></span> contact(s)
</span>
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<select class="form-input" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" x-model="bulkGroupId" @focus="fetchGroups()">
<option value="">-- Add to Existing Group --</option>
<template x-for="grp in groups" :key="grp.id">
<option :value="grp.id" x-text="grp.name"></option>
</template>
</select>
<span style="color: var(--text-secondary); font-size: 0.85rem;">or</span>
<input type="text" class="form-input" style="width: 180px; padding: 0.5rem 1rem; font-size: 0.85rem;" placeholder="New Group Name" x-model="bulkNewGroupName">
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="applyBulkGroup()">Apply Grouping</button>
</div>
</div>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px; text-align: center;">
<input type="checkbox" @change="selectedContactIds = $event.target.checked ? contacts.map(c => c.id) : []" :checked="contacts.length > 0 && selectedContactIds.length === contacts.length" style="cursor: pointer; width: 16px; height: 16px;">
</th>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
@@ -859,6 +880,9 @@
<tbody>
<template x-for="contact in contacts" :key="contact.id">
<tr>
<td style="text-align: center;">
<input type="checkbox" :value="contact.id" x-model="selectedContactIds" style="cursor: pointer; width: 16px; height: 16px;">
</td>
<td class="font-semibold" x-text="contact.name"></td>
<td x-text="contact.phone || 'N/A'"></td>
<td x-text="contact.email || 'N/A'"></td>
@@ -1154,6 +1178,9 @@
activeDashboardTab: 'whatsapp',
whatsappSession: null,
contacts: [],
selectedContactIds: [],
bulkGroupId: '',
bulkNewGroupName: '',
templates: [],
campaigns: [],
pollingIntervalId: null,
@@ -1397,6 +1424,9 @@
// CRM List Fetchers
async fetchContacts() {
this.selectedContactIds = [];
this.bulkGroupId = '';
this.bulkNewGroupName = '';
try {
const response = await fetch('/api/contacts', {
headers: { 'Authorization': `Bearer ${this.token}` }
@@ -1410,6 +1440,67 @@
}
},
async applyBulkGroup() {
if (this.selectedContactIds.length === 0) {
alert('Please select at least one contact first.');
return;
}
if (!this.bulkGroupId && !this.bulkNewGroupName.trim()) {
alert('Please select an existing group or type a new group name.');
return;
}
this.actionLoading = true;
try {
let groupId = this.bulkGroupId;
// If a new group name is typed, create the group first
if (this.bulkNewGroupName.trim()) {
const newGroupRes = await fetch('/api/groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({ name: this.bulkNewGroupName.trim() })
});
const newGroupData = await newGroupRes.json();
if (!newGroupRes.ok) {
throw new Error(newGroupData.message || newGroupData.error || 'Failed to create group');
}
groupId = newGroupData.id;
}
// Attach selected contacts in bulk
const bulkRes = await fetch('/api/groups/bulk-add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
group_id: groupId,
contact_ids: this.selectedContactIds
})
});
const bulkData = await bulkRes.json();
if (bulkRes.ok) {
alert('Successfully added selected contacts to the group!');
this.selectedContactIds = [];
this.bulkGroupId = '';
this.bulkNewGroupName = '';
await this.fetchGroups();
} else {
alert(bulkData.error || bulkData.message || 'Failed to apply grouping.');
}
} catch (err) {
console.error('Error applying bulk grouping:', err);
alert(err.message || 'An error occurred during bulk grouping.');
} finally {
this.actionLoading = false;
}
},
async fetchTemplates() {
try {
const response = await fetch('/api/templates', {

View File

@@ -56,6 +56,7 @@ $router->post('/api/contacts', [\App\Controllers\ContactController::class, '
$router->get('/api/groups', [\App\Controllers\GroupController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/groups', [\App\Controllers\GroupController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/groups/add', [\App\Controllers\GroupController::class, 'addContact'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/groups/bulk-add', [\App\Controllers\GroupController::class, 'bulkAddContacts'], [\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/templates', [\App\Controllers\TemplateController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/templates', [\App\Controllers\TemplateController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);