Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions addon/controllers/promotions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default class PromotionsController extends Controller {
@service intl;

get tabs() {
return [
{
route: 'promotions.push-notifications',
label: this.intl.t('storefront.promotions.push-notifications.tab-title'),
icon: 'bell',
},
];
}
}
64 changes: 64 additions & 0 deletions addon/controllers/promotions/push-notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class PromotionsPushNotificationsController extends Controller {
@service fetch;
@service notifications;
@service intl;
@service currentUser;
@service storefront;

@tracked title = '';
@tracked body = '';
@tracked selectedCustomers = [];
@tracked selectAllCustomers = false;
@tracked isLoading = false;

@action
async sendPushNotification(event) {
event.preventDefault();

// Validate form
if (!this.title || !this.body) {
this.notifications.warning(this.intl.t('storefront.promotions.push-notifications.validation-title-body-required'));
return;
}

if (!this.selectAllCustomers && (!this.selectedCustomers || this.selectedCustomers.length === 0)) {
this.notifications.warning(this.intl.t('storefront.promotions.push-notifications.validation-customers-required'));
return;
}

this.isLoading = true;

try {
const payload = {
title: this.title,
body: this.body,
store: this.storefront.getActiveStore('public_id'),
select_all: this.selectAllCustomers,
};

// Only include customer IDs if not selecting all
if (!this.selectAllCustomers) {
payload.customers = this.selectedCustomers.map((customer) => customer.id);
}

await this.fetch.post('storefront/int/v1/actions/send-push-notification', payload);

this.notifications.success(this.intl.t('storefront.promotions.push-notifications.notification-sent-success'));

// Reset form
this.title = '';
this.body = '';
this.selectedCustomers = [];
this.selectAllCustomers = false;
} catch (error) {
this.notifications.error(this.intl.t('storefront.promotions.push-notifications.notification-sent-error'));
} finally {
this.isLoading = false;
}
}
}
4 changes: 3 additions & 1 deletion addon/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export default buildRoutes(function () {
this.route('food-trucks', function () {
this.route('index', { path: '/' }, function () {});
});
this.route('promotions');
this.route('promotions', function () {
this.route('push-notifications', { path: '/' });
});
this.route('coupons');
this.route('broadcast');
this.route('pages');
Expand Down
16 changes: 16 additions & 0 deletions addon/routes/promotions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class PromotionsRoute extends Route {
@service intl;
@service abilities;
@service hostRouter;
@service notifications;

beforeModel() {
if (this.abilities.cannot('storefront view promotions')) {
this.notifications.warning(this.intl.t('common.unauthorized-access'));
return this.hostRouter.transitionTo('console');
}
}
}
19 changes: 19 additions & 0 deletions addon/routes/promotions/push-notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class PromotionsPushNotificationsRoute extends Route {
@service store;
@service currentUser;
@service storefront;
@service intl;
@service abilities;
@service hostRouter;
@service notifications;

beforeModel() {
if (this.abilities.cannot('storefront send push notifications')) {
this.notifications.warning(this.intl.t('common.unauthorized-access'));
return this.hostRouter.transitionTo('console.storefront');
}
}
}
7 changes: 7 additions & 0 deletions addon/templates/application.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
@visible="storefront see food-truck"
disabled={{not this.activeStore.id}}
>{{t "storefront.sidebar.food-trucks"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item
@route="console.storefront.promotions"
@icon="bullhorn"
@permission="storefront view promotions"
@visible="storefront see promotions"
disabled={{not this.activeStore.id}}
>{{t "storefront.sidebar.promotions"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item
@route="console.storefront.settings"
@icon="cogs"
Expand Down
8 changes: 8 additions & 0 deletions addon/templates/promotions.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="px-4 flex-row-reverse">
<:actions>
<h3 class="uppercase text-xs tracking-wide text-gray-700 dark:text-gray-400 font-semibold">{{t "storefront.promotions.title"}}</h3>
</:actions>
<:default>
{{outlet}}
</:default>
</TabNavigation>
70 changes: 70 additions & 0 deletions addon/templates/promotions/push-notifications.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<div class="overflow-y-scroll h-screen">
<div class="h-screen">
<main class="h-screen max-w-xl mx-auto pt-4">
<form class="h-screen" {{on "submit" this.sendPushNotification}}>
<div class="space-y-4 h-screen">
<div>
<h1 class="text-lg leading-6 font-bold text-gray-900 dark:text-gray-100">
{{t "storefront.promotions.push-notifications.header"}}
</h1>
<p class="mt-1 text-sm text-gray-500">
{{t "storefront.promotions.push-notifications.description"}}
</p>
</div>

<InputGroup
@name={{t "storefront.promotions.push-notifications.title-label"}}
@value={{this.title}}
@placeholder={{t "storefront.promotions.push-notifications.title-placeholder"}}
@helpText={{t "storefront.promotions.push-notifications.title-help-text"}}
/>

<InputGroup
@name={{t "storefront.promotions.push-notifications.body-label"}}
@type="textarea"
@value={{this.body}}
@placeholder={{t "storefront.promotions.push-notifications.body-placeholder"}}
@helpText={{t "storefront.promotions.push-notifications.body-help-text"}}
/>

<div class="mb-4">
<Toggle @isToggled={{this.selectAllCustomers}} @onToggle={{fn (mut this.selectAllCustomers)}} @wrapperClass="justify-start">
<span class="text-sm font-semibold dark:text-gray-100">{{t "storefront.promotions.push-notifications.select-all-customers"}}</span>
<span class="text-xs text-gray-500 block">{{t "storefront.promotions.push-notifications.select-all-customers-help-text"}}</span>
</Toggle>
</div>

{{#unless this.selectAllCustomers}}
<InputGroup @name={{t "storefront.promotions.push-notifications.customers-label"}}>
<ModelSelectMultiple
@modelName="contact"
@query={{hash type="customer"}}
@selectedModel={{this.selectedCustomers}}
@placeholder={{t "storefront.promotions.push-notifications.customers-placeholder"}}
@triggerClass="form-select form-input multiple"
@infiniteScroll={{true}}
@renderInPlace={{true}}
@onChange={{fn (mut this.selectedCustomers)}}
as |model|
>
{{model.name}}
</ModelSelectMultiple>
</InputGroup>
{{/unless}}

<div class="flex justify-end">
<Button
@type="primary"
@icon={{if this.isLoading "spinner" "paper-plane"}}
@iconPrefix={{if this.isLoading "fas fa-spin" "fas"}}
@text={{t "storefront.promotions.push-notifications.send-button"}}
@isLoading={{this.isLoading}}
@disabled={{this.isLoading}}
@onClick={{this.sendPushNotification}}
/>
</div>
</div>
</form>
</main>
</div>
</div>
1 change: 1 addition & 0 deletions app/controllers/promotions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/storefront-engine/controllers/promotions';
1 change: 1 addition & 0 deletions app/controllers/promotions/push-notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/storefront-engine/controllers/promotions/push-notifications';
1 change: 1 addition & 0 deletions app/routes/promotions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/storefront-engine/routes/promotions';
1 change: 1 addition & 0 deletions app/routes/promotions/push-notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/storefront-engine/routes/promotions/push-notifications';
1 change: 1 addition & 0 deletions app/templates/promotions.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{outlet}}
1 change: 1 addition & 0 deletions app/templates/promotions/push-notifications.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{outlet}}
61 changes: 61 additions & 0 deletions server/src/Http/Controllers/ActionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,65 @@ public function getMetrics(Request $request)

return response()->json($metrics);
}

/**
* Send promotional push notification to selected customers.
*
* @return \Illuminate\Http\Response
*/
public function sendPushNotification(Request $request)
{
$title = $request->input('title');
$body = $request->input('body');
$customerIds = $request->input('customers', []);
$storeId = $request->input('store');
$selectAll = $request->boolean('select_all', false);

// Validate inputs
if (!$title || !$body) {
return response()->json(['error' => 'Title and body are required'], 400);
}

if (!$selectAll && empty($customerIds)) {
return response()->json(['error' => 'At least one customer must be selected'], 400);
}

// Get the store
$store = Store::where('public_id', $storeId)->first();
if (!$store) {
return response()->json(['error' => 'Store not found'], 404);
}

// Get customers
if ($selectAll) {
// Get all customers for this store's company
$customers = Contact::where('company_uuid', session('company'))
->where('type', 'customer')
->get();
} else {
// Get only selected customers
$customers = Contact::whereIn('uuid', $customerIds)
->where('company_uuid', session('company'))
->where('type', 'customer')
->get();
}

// Send notifications
$sentCount = 0;
foreach ($customers as $customer) {
try {
$customer->notify(new \Fleetbase\Storefront\Notifications\PromotionalPushNotification($title, $body, $store));
$sentCount++;
} catch (\Exception $e) {
// Log error but continue with other customers
\Log::error('Failed to send push notification to customer: ' . $customer->uuid, ['error' => $e->getMessage()]);
}
}

return response()->json([
'status' => 'OK',
'sent_count' => $sentCount,
'total' => count($customers),
]);
}
}
Loading
Loading