New function to update voucher end date display by admin

This commit is contained in:
Dillon Ngo
2026-07-03 12:30:54 +08:00
parent 46862b014a
commit 45319df160
16 changed files with 499 additions and 4 deletions
@@ -0,0 +1,45 @@
<?php
namespace App\Classes\Modules\Vouchers\ControllersLogic;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherCampaignEndDateObject;
use App\Classes\Modules\Vouchers\Services\UpdatesVoucherCampaignEndDate;
use App\Classes\Modules\Vouchers\Standards\Rules\CanUpdateVoucherCampaignEndDate;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Http\Resources\VoucherCampaignResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateVoucherCampaignEndDateLogic extends AbstractControllerLogic
{
/**
* UpdateVoucherCampaignEndDateLogic constructor.
* @param UpdatesVoucherCampaignEndDate $updatesVoucherCampaignEndDate
* @param CanUpdateVoucherCampaignEndDate $canUpdateVoucherCampaignEndDate
*/
public function __construct(private UpdatesVoucherCampaignEndDate $updatesVoucherCampaignEndDate, private CanUpdateVoucherCampaignEndDate $canUpdateVoucherCampaignEndDate)
{
}
protected function notification(): array
{
return [
'title' => 'Updated Voucher Campaign End Date',
'message' => 'The selected voucher campaign end date has been updated.'
];
}
public function logic(Request $request): JsonResponse
{
$object = new UpdateVoucherCampaignEndDateObject(
$request->input('campaign_id'),
$request->input('end_date')
);
$this->canUpdateVoucherCampaignEndDate->passes($object);
$campaign = $this->updatesVoucherCampaignEndDate->execute($object);
return $this->response([]);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Classes\Modules\Vouchers\ControllersLogic;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherEndDateObject;
use App\Classes\Modules\Vouchers\Services\UpdatesVoucherEndDate;
use App\Classes\Modules\Vouchers\Standards\Rules\CanUpdateVoucherEndDate;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateVoucherEndDateLogic extends AbstractControllerLogic
{
/** @var UpdatesVoucherEndDate */
private $updatesVoucherEndDate;
/** @var CanUpdateVoucherEndDate */
private $canUpdateVoucherEndDate;
/**
* UpdateVoucherEndDateLogic constructor.
* @param UpdatesVoucherEndDate $updatesVoucherEndDate
* @param CanUpdateVoucherEndDate $canUpdateVoucherEndDate
*/
public function __construct(UpdatesVoucherEndDate $updatesVoucherEndDate, CanUpdateVoucherEndDate $canUpdateVoucherEndDate)
{
$this->updatesVoucherEndDate = $updatesVoucherEndDate;
$this->canUpdateVoucherEndDate = $canUpdateVoucherEndDate;
}
protected function notification(): array
{
return [
'title' => 'Updated Voucher End Date',
'message' => 'The selected voucher end date has been updated.'
];
}
public function logic(Request $request): JsonResponse
{
$object = new UpdateVoucherEndDateObject(
$request->input('id'),
$request->input('end_date')
);
$this->canUpdateVoucherEndDate->passes($object);
$voucher = $this->updatesVoucherEndDate->execute($object);
return $this->response([]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Classes\Modules\Vouchers\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class UpdateVoucherCampaignEndDateObject implements DataTransferObject
{
/** @var string */
private $campaign_id;
/** @var string|null */
private $end_date;
public function __construct(string $campaign_id, ?string $end_date)
{
$this->campaign_id = $campaign_id;
$this->end_date = $end_date;
}
public function getCampaignId(): string
{
return $this->campaign_id;
}
public function getEndDate(): ?string
{
return $this->end_date;
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Classes\Modules\Vouchers\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class UpdateVoucherEndDateObject implements DataTransferObject
{
/** @var int */
private $voucher_id;
/** @var string|null */
private $end_date;
public function __construct(int $voucher_id, ?string $end_date)
{
$this->voucher_id = $voucher_id;
$this->end_date = $end_date;
}
public function getVoucherId(): int
{
return $this->voucher_id;
}
public function getEndDate(): ?string
{
return $this->end_date;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Classes\Modules\Vouchers\Services;
use App\Classes\General\Eloquent\AbstractUpdateRecord;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherCampaignEndDateObject;
use App\Models\VoucherCampaign;
class UpdatesVoucherCampaignEndDate extends AbstractUpdateRecord
{
public function execute(UpdateVoucherCampaignEndDateObject $object): VoucherCampaign
{
$campaign = VoucherCampaign::where('campaign_id', $object->getCampaignId())->firstOrFail();
$endDate = $object->getEndDate();
if ($endDate) {
$parsed = \Carbon\Carbon::createFromFormat('Y-m-d', $endDate)->endOfDay()->timezone('Asia/Kuala_Lumpur');
} else {
$parsed = null;
}
$campaign->vouchers()->update(['end_date' => $parsed]);
return $this->handler($campaign->fresh());
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Classes\Modules\Vouchers\Services;
use App\Classes\General\Eloquent\AbstractUpdateRecord;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherEndDateObject;
use App\Models\Voucher;
class UpdatesVoucherEndDate extends AbstractUpdateRecord
{
public function execute(UpdateVoucherEndDateObject $object): Voucher
{
$voucher = Voucher::where('id', $object->getVoucherId())->firstOrFail();
if ($object->getEndDate()) {
$parsed = \Carbon\Carbon::createFromFormat('Y-m-d', $object->getEndDate())->endOfDay()->timezone('Asia/Kuala_Lumpur');
} else {
$parsed = null;
}
$voucher->end_date = $parsed;
$voucher->save();
return $this->handler($voucher->fresh());
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Classes\Modules\Vouchers\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherCampaignEndDateObject;
use Illuminate\Support\Facades\Auth;
use App\Classes\ValueObjects\Constants\RoleTypes;
class CanUpdateVoucherCampaignEndDate extends AbstractRule
{
/**
* @param UpdateVoucherCampaignEndDateObject $object
* @return bool
*/
protected function authorized($object): bool
{
return in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES);
}
/**
* @param UpdateVoucherCampaignEndDateObject $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
if (empty($object->getCampaignId()) || $object->getEndDate() === '') {
return true;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $object->getEndDate())) {
throw new \App\Classes\Exceptions\RequestValidationException('The end date format is invalid.');
}
return true;
}
/**
* @param UpdateVoucherCampaignEndDateObject $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Classes\Modules\Vouchers\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\Vouchers\DataTransferObjects\UpdateVoucherEndDateObject;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanUpdateVoucherEndDate extends AbstractRule
{
/**
* @param UpdateVoucherEndDateObject $object
* @return bool
*/
protected function authorized($object): bool
{
return in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES);
}
/**
* @param UpdateVoucherEndDateObject $object
* @return bool
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
protected function validators($object): bool
{
if (empty($object->getVoucherId()) || $object->getEndDate() === '') {
return true;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $object->getEndDate())) {
throw new \App\Classes\Exceptions\RequestValidationException('The end date format is invalid.');
}
return true;
}
/**
* @param UpdateVoucherEndDateObject $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Vouchers;
use App\Classes\Modules\Vouchers\ControllersLogic\UpdateVoucherCampaignEndDateLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateVoucherCampaignEndDateController
{
/**
* @param Request $request
* @param UpdateVoucherCampaignEndDateLogic $logic
* @return JsonResponse
*/
public function update(Request $request, UpdateVoucherCampaignEndDateLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Vouchers;
use App\Classes\Modules\Vouchers\ControllersLogic\UpdateVoucherEndDateLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateVoucherEndDateController
{
/**
* @param Request $request
* @param UpdateVoucherEndDateLogic $logic
* @return JsonResponse
*/
public function update(Request $request, UpdateVoucherEndDateLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -27,6 +27,7 @@ class VoucherCampaignResource extends JsonResource
'slug' => $this->slug,
'description' => $this->description,
'limit_per_month' => $this->limit_per_month,
'end_date' => optional($this->vouchers()->latest('end_date')->first())->end_date,
'metadata' => $metadata
];
}
+4
View File
@@ -38,6 +38,10 @@ class VoucherResource extends JsonResource
'end_date' => $this->end_date,
'is_redeemed' => $filteredRedemptions->count() > 0,
'email' => $this->email ? new KeyValueBasicResource($this->email->where('key', $this->code.'_EMAIL_COUNT')->first()) : null,
'campaign' => $this->campaign ? [
'name' => $this->campaign->name,
// 'campaign_id' => $this->campaign->campaign_id,
] : null,
];
}
}
-2
View File
@@ -1,5 +1,3 @@
version: '3'
networks:
exchange-development:
@@ -14,8 +14,28 @@
<div class="col-md-3">
<p v-if="item.voucher && item.voucher.is_redeemed" class="text-secondary">{{ item.voucher.code }}</p>
<p v-else class="text-primary">{{ item.voucher.code }}</p>
<p v-if="item.voucher && item.voucher.campaign" class="text-muted fs-12">
Campaign: {{ item.voucher.campaign.name }}
</p>
</div>
<div class="col-md-4">
<div v-if="!item.voucher.is_redeemed && authStore.isAdmin" class="row">
<div class="col-md">
<input type="date" class="form-control" v-model="endDate" @change="onDateChange">
</div>
<div class="col-md d-flex align-items-center">
<button class="btn btn-sm btn-primary" :disabled="!canUpdateEndDate" @click="updateEndDate">
Update End Date
</button>
<span v-tooltip:top="'This only updates the display end date. The actual business logic end date must be updated in the Voucherify Dashboard.'" class="info-tooltip ml-2" style="cursor: pointer;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>
</span>
</div>
</div>
</div>
<div class="col-md-4"></div>
<div class="col-md-1">
<a class="pointer requestModal" :data-type="'showVoucherTnC-' + item.id">Terms</a>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" size="extra-large" :type="'showVoucherTnC-' + item.id">
@@ -49,7 +69,12 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useComponentHandler } from '@/composables/useComponentHandler';
import { useAuthStore } from '@/stores/auth';
import { useQueueStore } from '@/stores/queue';
import { useRequest } from '@/composables/useRequest';
import route from '@/general/functions/router';
interface Props {
data: Record<string, any>;
@@ -57,4 +82,50 @@ interface Props {
const props = defineProps<Props>();
const { item } = useComponentHandler(props);
const authStore = useAuthStore();
const queueStore = useQueueStore();
const { submit } = useRequest();
const endDate = ref('');
const hasUserSelected = ref(false);
watch(
() => item.value?.voucher?.end_date,
(endDateValue) => {
endDate.value = endDateValue ? endDateValue.split('T')[0] : '';
hasUserSelected.value = false;
},
{ immediate: true }
);
const onDateChange = () => {
hasUserSelected.value = true;
};
const canUpdateEndDate = computed(() => Boolean(endDate.value) && hasUserSelected.value);
const updateEndDate = async () => {
if (!canUpdateEndDate.value || !item.value?.voucher?.id) {
return;
}
await submit(
route('api.voucher.update.end_date', item.value.voucher.id),
'post',
'customerVouchersListSection',
true,
true,
{
id: item.value.voucher.id,
end_date: endDate.value
},
{
successHandler: () => {
queueStore.reloadList({ name: 'customerVouchersListSection' });
queueStore.reloadList({ name: 'customerRewardsListSection' });
queueStore.reloadList({ name: 'customerUsedVouchersListSection' });
}
}
);
};
</script>
@@ -6,7 +6,7 @@
<div class="font-heading fs-16 all-caps bold m-b-15">Voucher Campaigns ALL CUSTOMERS</div>
</div>
</div>
<div class="row">
<div class="row m-b-10">
<div class="col">
<div class="row padding-10">
<div class="col-2 fs-10">Name</div>
@@ -23,18 +23,91 @@
</list-component>
</div>
</div>
<div class="row m-b-10">
<div class="col">
<div class="font-heading fs-16 all-caps bold m-b-15">Update Campaign End Date</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<select class="form-control" v-model="selectedCampaignId">
<option value="">Select campaign</option>
<option v-for="campaign in campaigns" :key="campaign.campaign_id" :value="campaign.campaign_id">
{{ campaign.name }} ({{ campaign.campaign_id }})
</option>
</select>
</div>
<div class="col-md-3">
<input type="date" class="form-control" v-model="selectedEndDate">
</div>
<div class="col-md-3 d-flex align-items-center">
<button class="btn btn-primary" :disabled="!canUpdateCampaign" @click="updateCampaignEndDate">
Update
</button>
<span v-tooltip:top="'This only updates the display end date. The actual business logic end date must be updated in the Voucherify Dashboard.'" class="info-tooltip ml-2" style="cursor: pointer;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import { useAuthStore } from '@/stores/auth';
import route from '@/general/functions/router';
import { useQueueStore } from '@/stores/queue';
import { useRequest } from '@/composables/useRequest';
const authStore = useAuthStore();
const queueStore = useQueueStore();
const { submit } = useRequest();
const campaigns = ref<Record<string, any>[]>([]);
const selectedCampaignId = ref('');
const selectedEndDate = ref('');
const options = {
include_metadata: true,
is_display: 1
};
const canUpdateCampaign = computed(() => Boolean(selectedCampaignId.value && selectedEndDate.value));
onMounted(async () => {
await queueStore.fetchSelectable({ section: 'voucherCampaignsList', endpoint: route('api.voucher.campaigns.list') });
campaigns.value = queueStore.getSelectableList('voucherCampaignsList');
});
const updateCampaignEndDate = async () => {
if (!canUpdateCampaign.value) {
return;
}
await submit(
route('api.voucher.campaigns.update.end_date'),
'post',
'voucherCampaignsSection',
true,
true,
{
campaign_id: selectedCampaignId.value,
end_date: selectedEndDate.value
},
{
successHandler: () => {
queueStore.reloadList({ name: 'voucherCampaignsSection' });
queueStore.reloadList({ name: 'customerVouchersListSection' });
queueStore.reloadList({ name: 'customerRewardsListSection' });
queueStore.reloadList({ name: 'customerUsedVouchersListSection' });
selectedCampaignId.value = '';
selectedEndDate.value = '';
}
}
);
};
</script>
+4
View File
@@ -4,6 +4,8 @@ use App\Http\Controllers\Vouchers\ValidateVoucherController;
use App\Http\Controllers\Vouchers\CreateVoucherController;
use App\Http\Controllers\Vouchers\ListUserVouchersController;
use App\Http\Controllers\Vouchers\ListVoucherCampaignsController;
use App\Http\Controllers\Vouchers\UpdateVoucherCampaignEndDateController;
use App\Http\Controllers\Vouchers\UpdateVoucherEndDateController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'voucher', 'as' => 'voucher.'], function () {
@@ -12,4 +14,6 @@ Route::group(['prefix' => 'voucher', 'as' => 'voucher.'], function () {
// Route::post('redeem', [RedeemVoucherController::class, 'redeem'])->name('redeem');
Route::get('/user/list', [ListUserVouchersController::class, 'list'])->name('user.list');
Route::get('/voucher/campaigns/list', [ListVoucherCampaignsController::class, 'list'])->name('campaigns.list');
Route::post('/voucher/campaigns/end-date', [UpdateVoucherCampaignEndDateController::class, 'update'])->name('campaigns.update.end_date');
Route::post('/{id}/end-date', [UpdateVoucherEndDateController::class, 'update'])->name('update.end_date');
});