Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,127 @@
|
||||
<div class="space-y-4">
|
||||
<!-- Countries -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Countries') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-24 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedCountries(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple countries') }}">
|
||||
@foreach($countries->sortBy(function ($country) { return $country->translations->first()->name; }) as $country)
|
||||
<option value="{{ $country->id }}" @selected(in_array($country->id, $selectedCountries))>
|
||||
{{ $country->flag }} {{ $country->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedCountries) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedCountries)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Divisions -->
|
||||
@if($divisions->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Divisions / States / Provinces') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-20 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedDivisions(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple divisions') }}">
|
||||
@foreach($divisions->sortBy(function ($division) { return $division->translations->first()->name; }) as $division)
|
||||
<option value="{{ $division->id }}" @selected(in_array($division->id, $selectedDivisions))>
|
||||
{{ $division->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedDivisions) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedDivisions)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Cities -->
|
||||
@if($cities->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Cities') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-32 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedCities(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple cities') }}">
|
||||
@foreach($cities->sortBy(function ($city) { return $city->translations->first()->name; }) as $city)
|
||||
<option value="{{ $city->id }}" @selected(in_array($city->id, $selectedCities))>
|
||||
{{ $city->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedCities) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedCities)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Districts -->
|
||||
@if($districts->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Districts / Neighborhoods') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-24 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedDistricts(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple districts') }}">
|
||||
@foreach($districts->sortBy(function ($district) { return $district->translations->first()->name; }) as $district)
|
||||
<option value="{{ $district->id }}" @selected(in_array($district->id, $selectedDistricts))>
|
||||
{{ $district->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedDistricts) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedDistricts)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Clear button -->
|
||||
@if(count($selectedCountries) > 0 || count($selectedDivisions) > 0 || count($selectedCities) > 0 || count($selectedDistricts) > 0)
|
||||
<div class="pt-2">
|
||||
<button type="button" wire:click="resetLocationFilter"
|
||||
class="text-sm text-red-600 hover:text-red-800 flex items-center space-x-1">
|
||||
<x-icon name="x-mark" class="w-4 h-4" />
|
||||
<span>{{ __('Clear all location filters') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Selection summary -->
|
||||
@if(count($selectedCountries) > 0 || count($selectedDivisions) > 0 || count($selectedCities) > 0 || count($selectedDistricts) > 0)
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">{{ __('Active Location Filters:') }}</h4>
|
||||
<div class="space-y-1 text-xs text-gray-600">
|
||||
@if(count($selectedCountries) > 0)
|
||||
<div>{{ __('Countries: :count selected', ['count' => count($selectedCountries)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedDivisions) > 0)
|
||||
<div>{{ __('Divisions: :count selected', ['count' => count($selectedDivisions)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedCities) > 0)
|
||||
<div>{{ __('Cities: :count selected', ['count' => count($selectedCities)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedDistricts) > 0)
|
||||
<div>{{ __('Districts: :count selected', ['count' => count($selectedDistricts)]) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,651 @@
|
||||
<div class="mt-12">
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
@can('create mailings')
|
||||
<x-jetstream.button wire:click="openCreateModal" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('Create Mailing') }}
|
||||
</x-jetstream.button>
|
||||
@endcan
|
||||
|
||||
<!-- Bulk Delete Button -->
|
||||
@can('delete mailings')
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$bulkDisabled"
|
||||
class=""
|
||||
title="{{ __('Delete') . ' ' . __('selection') }}"
|
||||
wire:click.prevent="openBulkDeleteModal">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="mb-6">
|
||||
<!-- Search box -->
|
||||
<div class="mb-4 flex items-center">
|
||||
<div class="relative w-2/3">
|
||||
<input class="w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
||||
placeholder="{{ __('Search mailings') . '...' }}" type="text"
|
||||
wire:model.live="search">
|
||||
|
||||
@if ($search)
|
||||
<button class="absolute inset-y-0 right-0 flex items-center pr-3 text-theme-secondary hover:text-theme-primary focus:outline-none"
|
||||
wire:click.prevent="$set('search', '')">
|
||||
<x-icon mini name="backspace" solid />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<!-- Type Filter -->
|
||||
<div>
|
||||
<x-select :clearable="true" class="!w-60"
|
||||
placeholder="{{ __('Type') }}" wire:model.live="typeFilter">
|
||||
<x-select.option label="{{ __('Local Newsletter') }}" value="local_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter') }}" value="general_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter only subscription') }}" value="general_newsletter_only" />
|
||||
<x-select.option label="{{ __('System Message') }}" value="system_message" />
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<x-select :clearable="true" class="!w-60"
|
||||
placeholder="{{ __('Status') }}" wire:model.live="statusFilter">
|
||||
<x-select.option label="{{ __('Draft') }}" value="draft" />
|
||||
<x-select.option label="{{ __('Scheduled') }}" value="scheduled" />
|
||||
<x-select.option label="{{ __('Sending') }}" value="sending" />
|
||||
<x-select.option label="{{ __('Sent') }}" value="sent" />
|
||||
<x-select.option label="{{ __('Cancelled') }}" value="cancelled" />
|
||||
</x-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mailings Table -->
|
||||
<div class="bg-white shadow-sm rounded-lg" x-data="{
|
||||
initScrollSync() {
|
||||
if (this.$refs.topScroll && this.$refs.bottomScroll && this.$refs.table && this.$refs.topScrollContent) {
|
||||
const topScroll = this.$refs.topScroll;
|
||||
const bottomScroll = this.$refs.bottomScroll;
|
||||
const table = this.$refs.table;
|
||||
|
||||
// Set the width of the dummy div to match table width
|
||||
this.$refs.topScrollContent.style.width = table.scrollWidth + 'px';
|
||||
}
|
||||
}
|
||||
}" x-init="setTimeout(() => initScrollSync(), 100)">
|
||||
|
||||
<!-- Top scrollbar -->
|
||||
<div class="overflow-x-auto border-b border-gray-200" x-ref="topScroll" @scroll="$refs.bottomScroll.scrollLeft = $event.target.scrollLeft">
|
||||
<div x-ref="topScrollContent" style="height: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Table with bottom scrollbar -->
|
||||
<div class="overflow-x-auto" x-ref="bottomScroll" @scroll="$refs.topScroll.scrollLeft = $event.target.scrollLeft">
|
||||
<table class="min-w-full divide-y divide-gray-200" x-ref="table">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<!-- Select all checkbox could go here -->
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('id')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('ID') }}</span>
|
||||
@if($sortField === 'id')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle w-32"
|
||||
wire:click="sortBy('title')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Title') }}</span>
|
||||
@if($sortField === 'title')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle w-20"
|
||||
wire:click="sortBy('type')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Type') }}</span>
|
||||
@if($sortField === 'type')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('status')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Status') }}</span>
|
||||
@if($sortField === 'status')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
|
||||
{{ __('Recipients') }}
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('scheduled_at')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Scheduled') }}</span>
|
||||
@if($sortField === 'scheduled_at')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('updated_at')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Updated') }}</span>
|
||||
@if($sortField === 'updated_at')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle w-32">
|
||||
{{ __('Actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($mailings as $mailing)
|
||||
<tr>
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<input type="checkbox" value="{{ $mailing->id }}" wire:model.live="bulkSelected" class="rounded border-gray-300 text-theme-brand focus:border-theme-accent focus:ring-1 focus:ring-theme-accent">
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{{ $mailing->id }}
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ $mailing->title }}</div>
|
||||
<div class="text-sm text-gray-500">{{ Str::limit($mailing->getSubjectForLocale(), 50) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
{{ ucfirst(str_replace('_', ' ', $mailing->type)) }}
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full
|
||||
@if($mailing->status === 'draft') bg-gray-100 text-gray-800
|
||||
@elseif($mailing->status === 'scheduled') bg-yellow-100 text-yellow-800
|
||||
@elseif($mailing->status === 'sending') bg-blue-100 text-blue-800
|
||||
@elseif($mailing->status === 'sent') bg-green-100 text-green-800
|
||||
@elseif($mailing->status === 'aborted') bg-red-100 text-red-800
|
||||
@else bg-theme-danger-light text-theme-danger-dark @endif">
|
||||
{{ ucfirst($mailing->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ number_format($mailing->getEffectiveRecipientsCount()) }}
|
||||
@if($mailing->sent_count > 0)
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ number_format($mailing->sent_count) }} {{ __('sent') }}
|
||||
@if($mailing->failed_count > 0)
|
||||
<br>{{ number_format($mailing->failed_count) }} {{ __('failed') }}
|
||||
@endif
|
||||
@if($mailing->bounced_count > 0)
|
||||
<br>{{ number_format($mailing->bounced_count) }} {{ __('bounced') }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
@if($mailing->scheduled_at)
|
||||
<div class="leading-tight">
|
||||
<div>{{ $mailing->scheduled_at->format('M j') }}</div>
|
||||
<div>{{ $mailing->scheduled_at->format('Y') }}</div>
|
||||
<div class="text-xs">{{ $mailing->scheduled_at->format('H:i') }}</div>
|
||||
</div>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($mailing->updatedByUser)
|
||||
<div class="relative block cursor-pointer flex-shrink-0"
|
||||
onclick="window.location='{{ $mailing->updatedByUser->profile_url }}'"
|
||||
title="{{ $mailing->updatedByUser->name }}">
|
||||
<img class="h-6 w-6 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600"
|
||||
src="{{ $mailing->updatedByUser->profile_photo_path ? Storage::url($mailing->updatedByUser->profile_photo_path) : Storage::url(timebank_config('profiles.user.profile_photo_path_default')) }}"
|
||||
alt="profile">
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-500 leading-tight">
|
||||
<div>{{ $mailing->updated_at->format('M j') }}</div>
|
||||
<div>{{ $mailing->updated_at->format('Y') }}</div>
|
||||
<div class="text-xs">{{ $mailing->updated_at->format('H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex items-center justify-start space-x-1">
|
||||
|
||||
<!-- Edit Button -->
|
||||
@if(in_array($mailing->status, ['draft', 'scheduled']))
|
||||
<x-jetstream.secondary-button
|
||||
class=""
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click="openEditModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Preview Button -->
|
||||
@if(in_array($mailing->status, ['draft', 'scheduled', 'sending', 'sent']) && !empty($mailing->content_blocks))
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="sendTestMail({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendTestMail({{ $mailing->id }})"
|
||||
class=""
|
||||
title="{{ __('Preview and Send test mailing emails') }}">
|
||||
<span wire:loading.remove wire:target="sendTestMail({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="document-magnifying-glass" />
|
||||
</span>
|
||||
<span wire:loading wire:target="sendTestMail({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5 animate-spin" name="arrow-path" />
|
||||
</span>
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Send Button -->
|
||||
@if($mailing->canBeSent())
|
||||
<x-jetstream.secondary-button wire:click="openSendConfirmModal({{ $mailing->id }})"
|
||||
:disabled="!Gate::allows('send mailings')"
|
||||
class=""
|
||||
title="{{ Gate::allows('send mailings') ? __('Send Now') : __('No permission to send mailings') }}">
|
||||
<x-icon class="h-5 w-5" name="paper-airplane" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Stop Button (while sending) -->
|
||||
@if($mailing->status === 'sending')
|
||||
<x-jetstream.danger-button
|
||||
wire:click="openStopConfirmModal({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="openStopConfirmModal({{ $mailing->id }})"
|
||||
:disabled="!Gate::allows('send mailings')"
|
||||
class=""
|
||||
title="{{ Gate::allows('send mailings') ? __('Stop sending') : __('No permission to send mailings') }}">
|
||||
<x-icon class="h-5 w-5" name="stop-circle" solid />
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
|
||||
<!-- Report Button (all statuses) -->
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="openReportModal({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="openReportModal({{ $mailing->id }})"
|
||||
class=""
|
||||
title="{{ __('View Report') }}">
|
||||
<span wire:loading.remove wire:target="openReportModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="list-bullet" />
|
||||
</span>
|
||||
<span wire:loading wire:target="openReportModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5 animate-spin" name="arrow-path" />
|
||||
</span>
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
||||
{{ __('No mailings found.') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($mailings->hasPages())
|
||||
<div class="bg-white shadow-sm rounded-lg mt-4">
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $mailings->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showCreateModal" wire:key="showCreateModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Create mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.create-edit-form')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeModals">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="saveMailing" class="ml-3 bg-theme-brand">
|
||||
@if(isset($editingMailing) && $editingMailing)
|
||||
{{ __('Update mailings') }}
|
||||
@else
|
||||
@if($scheduledAt)
|
||||
{{ __('Schedule mailing') }}
|
||||
@else
|
||||
{{ __('Save as draft') }}
|
||||
@endif
|
||||
@endif
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<x-jetstream.dialog-modal wire:model.live="showEditModal" wire:key="showEditModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.create-edit-form')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeModals">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="saveMailing" class="ml-3 bg-theme-brand">
|
||||
@if(isset($editingMailing) && $editingMailing)
|
||||
{{ __('Update mailing') }}
|
||||
@else
|
||||
@if($scheduledAt)
|
||||
{{ __('Schedule mailing') }}
|
||||
@else
|
||||
{{ __('Save as draft') }}
|
||||
@endif
|
||||
@endif
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Post Selector Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showPostSelector" wire:key="showPostSelector" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Select posts for mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.post-selector')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showPostSelector', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="addSelectedPosts" class="ml-3 bg-theme-brand" :disabled="count($selectedPostIds) === 0">
|
||||
{{ __('Add selected posts') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Test Mail Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showTestMailModal" wire:key="showTestMailModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Test mail status') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="whitespace-pre-line text-sm text-gray-700">
|
||||
{{ $testMailMessage }}
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.button wire:click="$set('showTestMailModal', false)" class="bg-theme-brand">
|
||||
{{ __('OK') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Test Email Selection Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showTestEmailSelectionModal" wire:key="showTestEmailSelectionModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Preview your mailing') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
<!-- Email Preview - Top section -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700">{{ __('Email Preview') }}</h3>
|
||||
@if($mailingForTest)
|
||||
@php $previewLocale = $this->resolvePreviewLocale(); @endphp
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600">
|
||||
{{ strtoupper($previewLocale) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex justify-center -mx-[19px] sm:mx-0" wire:ignore>
|
||||
<div class="border border-gray-300 rounded-3xl overflow-hidden bg-white shadow-inner shadow-2xl w-full sm:w-auto" style="max-width: 322px; height: 456px;">
|
||||
<iframe
|
||||
id="mailing-preview-frame"
|
||||
style="width:100%;height:100%;border:0;background:#f4f4f4;"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@media (max-width: 639px) {
|
||||
#mailing-preview-frame {
|
||||
width: 125% !important;
|
||||
height: 125% !important;
|
||||
transform: scale(0.8);
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Selection - Bottom section -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">{{ __('Select Recipients') }}</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Choose which email addresses should receive the test mailing.') }}
|
||||
</p>
|
||||
|
||||
<!-- Available email options -->
|
||||
<div class="space-y-3">
|
||||
@if(isset($availableTestEmails['auth_user']))
|
||||
<label class="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model="sendToAuthUser"
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
|
||||
>
|
||||
<span class="text-sm text-gray-700">
|
||||
{{ $availableTestEmails['auth_user']['label'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
@if(isset($availableTestEmails['active_profile']))
|
||||
<label class="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model="sendToActiveProfile"
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
|
||||
>
|
||||
<span class="text-sm text-gray-700">
|
||||
{{ $availableTestEmails['active_profile']['label'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<!-- Custom email input -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">
|
||||
{{ __('Custom email address(es), separated by comma:') }}
|
||||
</label>
|
||||
<div>
|
||||
<x-input
|
||||
wire:model="customTestEmail"
|
||||
type="text"
|
||||
placeholder="{{ __('Enter email address(es)') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($mailingForTest)
|
||||
<div class="mt-4 p-3 bg-theme-surface rounded-md border border-theme-border">
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ __('Mailing:') }}</strong> {{ $mailingForTest->title }}
|
||||
</p>
|
||||
<p class="text-sm text-theme-secondary mt-1">
|
||||
{{ __('Test emails will be sent in all available languages for this mailing.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error('testRecipients')
|
||||
<p class="mt-3 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@error('customTestEmail')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="cancelTestEmailSelection" class="mr-3">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click="sendTestMailToSelected"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendTestMailToSelected"
|
||||
class="bg-theme-brand"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sendTestMailToSelected">
|
||||
{{ __('Send test mailing') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="sendTestMailToSelected">
|
||||
{{ __('Sending...') }}
|
||||
</span>
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Bulk Delete Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showBulkDeleteModal" wire:key="showBulkDeleteModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete selected mailings') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Are you sure you want to delete the selected mailings?') }}<br>
|
||||
{{ __('Only draft and scheduled mailings will be deleted. This action cannot be undone.') }}
|
||||
<br><br>
|
||||
<strong>{{ count($bulkSelected) }} {{ __('mailing(s) selected') }}</strong>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showBulkDeleteModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button wire:click="bulkDeleteMailings" class="ml-3">
|
||||
{{ __('Delete selected') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Stop Sending Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showStopConfirmModal" wire:key="showStopConfirmModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Stop sending') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if($mailingToStop)
|
||||
{{ __('Are you sure you want to stop this mailing?') }}<br>
|
||||
{{ __('Emails already sent cannot be recalled. This action cannot be undone.') }}
|
||||
<br><br>
|
||||
<strong>{{ $mailingToStop->title }}</strong>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showStopConfirmModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button wire:click="stopMailing" wire:loading.attr="disabled" class="ml-3">
|
||||
{{ __('Stop sending') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Report Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showReportModal" wire:key="showReportModal" maxWidth="3xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Mailing Report') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@if($mailingForReport)
|
||||
@if($mailingForReport->status === 'sending')
|
||||
<div wire:poll.5s="refreshReport">
|
||||
@include('livewire.mailings.partials.report-modal', ['mailing' => $mailingForReport])
|
||||
</div>
|
||||
@else
|
||||
@include('livewire.mailings.partials.report-modal', ['mailing' => $mailingForReport])
|
||||
@endif
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showReportModal', false)">
|
||||
{{ __('Close') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Send Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showSendConfirmModal" wire:key="showSendConfirmModal" maxWidth="2xl">
|
||||
|
||||
<x-slot name="title">
|
||||
{{ __('Send mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if($mailingToSend)
|
||||
{{ __('Are you sure you want to send this mailing?') }}<br>
|
||||
{{ __('This action cannot be undone and the mailing will be delivered immediately to all recipients.') }}
|
||||
<br><br>
|
||||
<strong>{{ __('Mailing') }}: {{ $mailingToSend->title }}</strong><br>
|
||||
<strong>{{ __('Recipients') }}: {{ number_format($mailingToSend->getEffectiveRecipientsCount()) }}</strong>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showSendConfirmModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="sendMailing" class="ml-3 bg-green-600">
|
||||
{{ __('Send Now') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,585 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Information -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<x-jetstream.label for="title" value="{{ __('Mailing title') }}" />
|
||||
<x-input id="title" type="text" class="mt-1 block w-full" wire:model.defer="title" />
|
||||
<x-jetstream.input-error for="title" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-jetstream.label for="type" value="{{ __('Mailing type') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</div>
|
||||
<x-select id="type" class="mt-1 w-full" wire:model.live="type">
|
||||
<x-select.option label="{{ __('Select mailing type...') }}" value="" />
|
||||
<x-select.option label="{{ __('Local Newsletter') }}" value="local_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter') }}" value="general_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter only subscription') }}" value="general_newsletter_only" />
|
||||
<x-select.option label="{{ __('System Message') }}" value="system_message" />
|
||||
</x-select>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@if($type === 'local_newsletter')
|
||||
{{ __('Sent to users based on their location preferences') }}
|
||||
@elseif($type === 'general_newsletter')
|
||||
{{ __('Sent to all subscribed users and organizations') }}
|
||||
@elseif($type === 'general_newsletter_only')
|
||||
{{ __('Sent to profiles subscribed to the General Newsletter but not to the Local Newsletter') }}
|
||||
@elseif($type === 'system_message')
|
||||
{{ __('System announcements and important notices') }}
|
||||
@else
|
||||
{{ __('Please select a mailing type to continue') }}
|
||||
@endif
|
||||
</p>
|
||||
<x-jetstream.input-error for="type" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Blocks -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<x-jetstream.label value="{{ __('Mailing content') }}" />
|
||||
<x-jetstream.secondary-button wire:click="openPostSelector" class="text-sm">
|
||||
<x-icon name="plus" class="w-4 h-4 mr-1" />
|
||||
{{ __('Add Posts') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
|
||||
@if(empty($selectedPosts))
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
|
||||
<x-icon name="document-text" class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500">{{ __('No posts selected yet') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($selectedPosts as $index => $post)
|
||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 bg-theme-brand text-white rounded-full text-xs flex items-center justify-center">
|
||||
{{ $post['order'] }}
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900">{{ $post['title'] }}</h4>
|
||||
<p class="text-sm text-gray-500">Post ID: {{ $post['post_id'] }}</p>
|
||||
@if (in_array($post['post_id'], $limitedPollPostIds ?? []))
|
||||
<p class="mt-1 text-sm text-amber-700">{{ __('This poll is only for some members. Recipients outside its audience will see it in the mailing but cannot take part.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Move Up -->
|
||||
@if($index > 0)
|
||||
<button wire:click="movePostUp({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<x-icon name="chevron-up" class="w-4 h-4" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Move Down -->
|
||||
@if($index < count($selectedPosts) - 1)
|
||||
<button wire:click="movePostDown({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<x-icon name="chevron-down" class="w-4 h-4" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Remove -->
|
||||
<button wire:click="removePost({{ $index }})"
|
||||
class="p-1 text-red-400 hover:text-red-600">
|
||||
<x-icon name="trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-jetstream.input-error for="selectedPosts" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Subject (Multilingual) — shown only when posts are selected -->
|
||||
@if(!empty($selectedPosts))
|
||||
<div>
|
||||
<x-jetstream.label value="{{ __('Email Subject') }}" />
|
||||
<p class="mt-1 text-sm text-gray-500 mb-3">{{ __('Enter subject lines for each language based on your selected posts') }}</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
@foreach($availableLocales as $locale)
|
||||
@php $isRequired = in_array($locale, $requiredLocales); @endphp
|
||||
<div class="border rounded-lg p-3 {{ $isRequired ? 'border-gray-200' : 'border-dashed border-gray-200' }}">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ strtoupper($locale) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
({{ $locale === 'en' ? 'English' :
|
||||
($locale === 'nl' ? 'Nederlands' :
|
||||
($locale === 'de' ? 'Deutsch' :
|
||||
($locale === 'es' ? 'Español' :
|
||||
($locale === 'fr' ? 'Français' : $locale)))) }})
|
||||
</span>
|
||||
@if($locale === timebank_config('base_language', 'en'))
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-theme-surface text-theme-primary">
|
||||
{{ __('Primary') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($isRequired)
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-700">
|
||||
{{ __('Required') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-500">
|
||||
{{ __('Optional') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<x-input
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="{{ __('Subject line in :language', ['language' => strtoupper($locale)]) }}"
|
||||
wire:model.defer="subjects.{{ $locale }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Scheduling (requires send mailings permission) -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Scheduling') }}" class="{{ !Gate::allows('send mailings') ? 'opacity-50' : '' }}" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="md:w-1/2 {{ !Gate::allows('send mailings') ? 'opacity-50 pointer-events-none' : '' }}"
|
||||
x-data
|
||||
x-on:flatpickr-change.window="$wire.set('scheduledAt', $event.detail.value)"
|
||||
x-on:flatpickr-set-date.window="
|
||||
const fp = document.querySelector('[wire\\:key=\'flatpickr-scheduled-at\'] .flatpickr-input')?._flatpickr;
|
||||
if (fp) { fp.setDate($event.detail.value || null, false); }
|
||||
">
|
||||
<div wire:ignore wire:key="flatpickr-scheduled-at">
|
||||
<x-flatpickr
|
||||
showTime
|
||||
dateFormat="Y-m-d"
|
||||
timeFormat="H:i"
|
||||
altFormat="d-m-Y @ H:i"
|
||||
altInput
|
||||
placeholder="{{ Gate::allows('send mailings') ? __('Select a date and time') : __('No permission to schedule mailings') }}"
|
||||
onClose="(selectedDates, dateStr) => window.dispatchEvent(new CustomEvent('flatpickr-change', { detail: { value: dateStr || null } }))"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<x-jetstream.input-error for="scheduledAt" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Type Filtering -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Profile Type Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Filter by Profile Type Toggle -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByProfileType" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter recipients by profile type') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByProfileType)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Select which profile types should receive this mailing. You can select multiple types.') }}
|
||||
</p>
|
||||
|
||||
<!-- Profile Type Selection -->
|
||||
<div class="space-y-3">
|
||||
<x-jetstream.label value="{{ __('Profile Types') }}" class="font-medium" />
|
||||
|
||||
<select multiple
|
||||
class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-32 focus:ring-blue-500 focus:border-blue-500"
|
||||
wire:model.live="selectedProfileTypes"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple profile types') }}">
|
||||
<option value="User">
|
||||
{{ __('Users') }} ({{ __('Individual profiles') }})
|
||||
</option>
|
||||
<option value="Organization">
|
||||
{{ __('Organizations') }}
|
||||
</option>
|
||||
<option value="Bank">
|
||||
{{ __('Banks') }}
|
||||
</option>
|
||||
<option value="Admin">
|
||||
{{ __('Admins') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
@if(count($selectedProfileTypes) > 0)
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($selectedProfileTypes as $profileType)
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-theme-surface text-theme-primary">
|
||||
@switch($profileType)
|
||||
@case('User')
|
||||
{{ __('Users') }}
|
||||
@break
|
||||
@case('Organization')
|
||||
{{ __('Organizations') }}
|
||||
@break
|
||||
@case('Bank')
|
||||
{{ __('Banks') }}
|
||||
@break
|
||||
@case('Admin')
|
||||
{{ __('Admins') }}
|
||||
@break
|
||||
@endswitch
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
<button type="button"
|
||||
wire:click="clearProfileTypes"
|
||||
class="text-xs text-gray-500 hover:text-gray-700">
|
||||
{{ __('Clear all') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ __('Tip: Hold Ctrl (Windows/Linux) or Cmd (Mac) while clicking to select multiple profile types') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if(count($selectedProfileTypes) > 0)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary">
|
||||
{{ __('Filtering by :count profile type(s)', ['count' => count($selectedProfileTypes)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Filtering -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Contact Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="filterByContacts,selectedContactProfiles" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByContacts" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter by contacts of...') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByContacts)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Only send to profiles that have transactions with the selected profile(s). The selected contacts will also be included.') }}
|
||||
</p>
|
||||
|
||||
<div wire:ignore>
|
||||
<livewire:profile.select-multiple-profiles
|
||||
wire:key="contact-profiles-selector-{{ $editingMailing->id ?? 'new' }}"
|
||||
:label="__('Select profiles')"
|
||||
:typesAvailable="[\App\Models\User::class, \App\Models\Organization::class, \App\Models\Bank::class]"
|
||||
eventName="selectedProfiles"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if(count($selectedContactProfiles) > 0)
|
||||
<div class="mt-3 space-y-2 max-w-md">
|
||||
@foreach($selectedContactProfiles as $index => $contactProfile)
|
||||
<div class="flex items-center justify-between rounded-md border border-theme-border bg-white px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<img src="{{ $contactProfile['profile_photo_path'] }}" class="w-8 rounded-full profile-photo">
|
||||
<div class="ml-2 leading-tight">
|
||||
<div class="text-sm font-semibold text-theme-primary">{{ $contactProfile['name'] }}</div>
|
||||
@php
|
||||
$contactMeta = implode(' · ', array_filter([$contactProfile['description'] ?? '', $contactProfile['location'] ?? '']));
|
||||
@endphp
|
||||
@if($contactMeta)
|
||||
<div class="text-xs text-theme-secondary">{{ $contactMeta }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="ml-3 text-theme-secondary hover:text-red-600 flex-shrink-0"
|
||||
wire:click="removeContactProfile({{ $index }})">
|
||||
<x-icon mini name="x-circle" />
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Filtering -->
|
||||
@if($type === 'local_newsletter')
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Location Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Filter by Location Toggle -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByLocation" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter recipients by location') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByLocation)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Select a location to filter recipients. Only profiles with their primary location in the selected area will receive this mailing.') }}
|
||||
</p>
|
||||
|
||||
<!-- Location Filter Component -->
|
||||
<div wire:ignore>
|
||||
<livewire:mailings.location-filter
|
||||
wire:key="mailing-location-filter-{{ $editingMailing->id ?? 'new' }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if(count($selectedCountryIds) > 0 || count($selectedDivisionIds) > 0 || count($selectedCityIds) > 0 || count($selectedDistrictIds) > 0)
|
||||
@php
|
||||
$locationFilteredCount = collect($this->getCurrentRecipientCountsByLocale())->sum('count');
|
||||
@endphp
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary">
|
||||
{{ __('Estimated recipients with location filter: :count', ['count' => number_format($locationFilteredCount)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Fallback Locale -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Fallback language for missing translations') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</div>
|
||||
<div class="md:w-1/2">
|
||||
@if(empty($fallbackEligibleLocales))
|
||||
<p class="text-sm text-gray-400 italic">{{ __('Select posts first to see available fallback languages.') }}</p>
|
||||
@else
|
||||
<x-select id="fallbackLocale" class="w-full" wire:model.live="fallbackLocale" placeholder="{{ __('Select an available language') }}">
|
||||
@foreach($fallbackEligibleLocales as $locale)
|
||||
@php $localeInfo = config('laravellocalization.supportedLocales.' . $locale); @endphp
|
||||
<x-select.option
|
||||
label="{{ strtoupper($locale) }}{{ $localeInfo ? ' — ' . $localeInfo['native'] : '' }}"
|
||||
value="{{ $locale }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Preview -->
|
||||
<div class="bg-theme-surface border border-theme-border rounded-lg p-4" wire:key="recipient-preview-{{ $type ?: 'no-type' }}">
|
||||
<h4 class="font-medium text-theme-primary mb-2">{{ __('Estimated Recipients') }}</h4>
|
||||
|
||||
@if($type)
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ __('messages.mailings.recipients_info.' . $type) }}
|
||||
@if($filterByLocation && (count($selectedCountryIds) > 0 || count($selectedDivisionIds) > 0 || count($selectedCityIds) > 0 || count($selectedDistrictIds) > 0))
|
||||
<br><strong>{{ __('messages.mailings.recipients_info.location_filtering_active') }}</strong>
|
||||
@endif
|
||||
</p>
|
||||
@else
|
||||
<p class="p-3">
|
||||
<strong>{{ __('messages.mailings.recipients_info.no_type_selected') }}</strong>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<!-- Real-time recipient count -->
|
||||
@if($type)
|
||||
@php
|
||||
$currentRecipientCount = collect($this->getCurrentRecipientCountsByLocale())->where('has_content', true)->sum('count');
|
||||
@endphp
|
||||
@if($currentRecipientCount > 0)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary flex items-center space-x-2">
|
||||
<span>{{ __('Current estimated recipients: :count', ['count' => number_format($currentRecipientCount)]) }}</span>
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-secondary flex items-center space-x-2">
|
||||
<span>{{ __('Current estimated recipients: 0') }}</span>
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($type)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">
|
||||
{{ __('Recipients by Language & Content') }}
|
||||
</p>
|
||||
|
||||
@php
|
||||
$rowsByLocaleLocation = $this->getCurrentRecipientCountsByLocaleAndLocation();
|
||||
@endphp
|
||||
|
||||
@if(count($rowsByLocaleLocation) > 0)
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr class="text-theme-secondary font-medium">
|
||||
<th style="text-align: left; padding: 0 8px 4px 0; font-weight: 500;">{{ __('Language') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500;">{{ __('Posts') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500; white-space: nowrap;">{{ __('Location') }}</th>
|
||||
<th style="text-align: right; padding: 0 0.5rem 4px 8px; font-weight: 500;">{{ __('Recipients') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$prevLocale = null;
|
||||
$fallbackLanguageName = \App\Models\Language::where('lang_code', $fallbackLocale)->value('name') ?? strtoupper($fallbackLocale);
|
||||
@endphp
|
||||
@foreach($rowsByLocaleLocation as $row)
|
||||
@php
|
||||
$isFirstForLocale = $row['locale'] !== $prevLocale;
|
||||
$prevLocale = $row['locale'];
|
||||
$language = $isFirstForLocale ? \App\Models\Language::where('lang_code', $row['locale'])->first() : null;
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
$languageName = $language ? $language->name : strtoupper($row['locale']);
|
||||
$nativePosts = $row['content_blocks'] - ($row['fallback_posts'] ?? 0);
|
||||
$fallbackPosts = $row['fallback_posts'] ?? 0;
|
||||
$missingPosts = $row['missing_posts'] ?? 0;
|
||||
@endphp
|
||||
<tr>
|
||||
{{-- Col 1: Language — only on first row for this locale --}}
|
||||
<td style="padding: 2px 8px 2px 0; white-space: nowrap;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>{{ $flag }}</span>
|
||||
<span class="font-medium text-theme-primary">{{ $languageName }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
{{-- Col 2: Posts — only on first row for this locale --}}
|
||||
<td style="padding: 2px 8px;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 4px; flex-wrap: nowrap;">
|
||||
@if($nativePosts > 0)
|
||||
<span class="font-medium rounded bg-green-100 text-green-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $nativePosts }} {{ __('native') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($fallbackPosts > 0)
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $fallbackPosts }} {{ __('fallback in') }} {{ $fallbackLanguageName }}
|
||||
</span>
|
||||
@endif
|
||||
@if($missingPosts > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $missingPosts }} {{ __('skipped') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($nativePosts === 0 && $fallbackPosts === 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ __('No content') }}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
{{-- Col 3: Location --}}
|
||||
<td class="text-theme-secondary" style="padding: 2px 8px; white-space: nowrap;">
|
||||
{{ $row['location'] ?: __('Unknown') }}
|
||||
</td>
|
||||
{{-- Col 4: Recipients --}}
|
||||
<td class="text-theme-secondary" style="padding: 2px 0.5rem 2px 8px; text-align: right;">
|
||||
{{ number_format($row['count']) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@php
|
||||
$totalRecipients = collect($rowsByLocaleLocation)->sum('count');
|
||||
$skippedRecipients = collect($rowsByLocaleLocation)->where('has_content', false)->sum('count');
|
||||
$incompleteRecipients = collect($rowsByLocaleLocation)->where('has_content', true)->where('all_posts', false)->sum('count');
|
||||
$suppressedCount = $this->getCurrentSuppressedCount();
|
||||
@endphp
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-theme-border">
|
||||
<div class="flex justify-between text-xs" style="padding-right: 0.5rem;">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total recipients:') }}</span>
|
||||
<span class="text-theme-secondary">{{ number_format($totalRecipients) }}</span>
|
||||
</div>
|
||||
@if($suppressedCount > 0)
|
||||
<div class="flex justify-between text-xs mt-1" style="padding-right: 0.5rem;">
|
||||
<span class="text-theme-secondary">{{ __('Skipped (hard bounce):') }}</span>
|
||||
<span class="font-medium text-gray-700">{{ number_format($suppressedCount) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Recipients skipped entirely (zero posts) --}}
|
||||
@if($skippedRecipients > 0)
|
||||
@if($fallbackLocale)
|
||||
<p class="text-xs text-theme-secondary mt-2">
|
||||
{{ __('Some recipients will not receive the mailing because no content exists in their language or the fallback language.') }}
|
||||
</p>
|
||||
@else
|
||||
<div class="mt-2 p-2 bg-red-50 border border-red-200 rounded">
|
||||
<p class="text-xs font-semibold text-red-700">
|
||||
{{ __(':count recipient(s) will be skipped — no content in their language.', ['count' => number_format($skippedRecipients)]) }}
|
||||
</p>
|
||||
<p class="text-xs text-red-600 mt-1">
|
||||
{{ __('Set a fallback language above to include them.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Recipients who get the mailing but miss some posts --}}
|
||||
@if($incompleteRecipients > 0 && !$fallbackLocale)
|
||||
<div class="mt-2 p-2 bg-amber-50 border border-amber-200 rounded">
|
||||
<p class="text-xs font-semibold text-amber-700">
|
||||
{{ __(':count recipient(s) will not receive all posts — some are missing a translation in their language.', ['count' => number_format($incompleteRecipients)]) }}
|
||||
</p>
|
||||
<p class="text-xs text-amber-600 mt-1">
|
||||
{{ __('Set a fallback language above to include the missing posts.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-theme-secondary">{{ __('No recipients found for this mailing type.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Validation errors summary -->
|
||||
<x-errors />
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<div class="space-y-4">
|
||||
<!-- Search -->
|
||||
<div>
|
||||
<x-label for="postSearch" value="{{ __('Search Posts') }}" />
|
||||
<x-input id="postSearch" type="text" class="mt-1 block w-full"
|
||||
wire:model.live="postSearch"
|
||||
placeholder="{{ __('Search by title...') }}" />
|
||||
</div>
|
||||
|
||||
<!-- Available Posts -->
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
@if($availablePosts && $availablePosts->count() > 0)
|
||||
<div class="space-y-2">
|
||||
@foreach($availablePosts as $post)
|
||||
@php
|
||||
$translation = $post->translations->firstWhere('locale', app()->getLocale())
|
||||
?? $post->translations->first();
|
||||
$isSelected = in_array($post->id, $selectedPostIds);
|
||||
@endphp
|
||||
|
||||
@if($translation)
|
||||
<div class="flex items-start justify-between p-4 border border-gray-200 rounded-lg
|
||||
{{ $isSelected ? 'bg-theme-surface border-theme-border' : 'bg-white hover:bg-gray-50' }}">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start space-x-3">
|
||||
<!-- Checkbox -->
|
||||
<input type="checkbox"
|
||||
class="mt-1 rounded border-gray-300 text-theme-brand focus:border-theme-brand focus:ring focus:ring-theme-brand"
|
||||
wire:change="togglePostSelection({{ $post->id }})"
|
||||
{{ $isSelected ? 'checked' : '' }}>
|
||||
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-gray-900">{{ $translation->title }}</h4>
|
||||
|
||||
@if($translation->excerpt)
|
||||
<p class="text-sm text-gray-500 mt-1">{{ Str::limit($translation->excerpt, 100) }}</p>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-400">
|
||||
<span>ID: {{ $post->id }}</span>
|
||||
|
||||
@if($post->category)
|
||||
@php $catTrans = $post->category->translations->firstWhere('locale', app()->getLocale()) ?? $post->category->translations->first(); @endphp
|
||||
<span>{{ $catTrans->name ?? __('Uncategorized') }}</span>
|
||||
@endif
|
||||
|
||||
<span>{{ $post->updated_at->format('M j, Y') }}</span>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$publishedTranslations = $this->getPublishedTranslationsWithFlags($post);
|
||||
@endphp
|
||||
|
||||
<div class="mt-1 text-xs">
|
||||
@if($publishedTranslations->count() > 0)
|
||||
<span class="text-green-600">
|
||||
@foreach($publishedTranslations as $pubTranslation)
|
||||
<span title="{{ $pubTranslation['locale'] }}">{{ $pubTranslation['flag'] }}</span>
|
||||
@endforeach
|
||||
</span>
|
||||
@else
|
||||
<span class="text-yellow-600">Not currently published</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Indicator -->
|
||||
@if($post->category)
|
||||
<div class="ml-3 self-start">
|
||||
@php
|
||||
$categoryId = $post->category->id;
|
||||
$catTrans = $post->category->translations->firstWhere('locale', app()->getLocale()) ?? $post->category->translations->first();
|
||||
$categoryName = $catTrans->name ?? __('Uncategorized');
|
||||
|
||||
// Determine color based on category type
|
||||
$colorClass = 'bg-gray-100 text-gray-800'; // default
|
||||
if(in_array($categoryId, [4, 8])) $colorClass = 'bg-theme-surface text-theme-primary'; // news
|
||||
elseif($categoryId === 5) $colorClass = 'bg-green-100 text-green-800'; // article
|
||||
elseif(in_array($categoryId, [6, 7])) $colorClass = 'bg-theme-danger-light text-theme-danger-dark'; // event
|
||||
@endphp
|
||||
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full {{ $colorClass }}">
|
||||
{{ $categoryName }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8">
|
||||
<x-icon name="document-text" class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500 mb-2">
|
||||
@if($postSearch)
|
||||
{{ __('No posts found matching your search.') }}
|
||||
@else
|
||||
{{ __('No published posts available.') }}
|
||||
@endif
|
||||
</p>
|
||||
@if($postSearch)
|
||||
<p class="text-sm text-gray-400">{{ __('Try a different search term or clear the search to see all posts.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Selected Count -->
|
||||
@if(count($selectedPostIds) > 0)
|
||||
<div class="bg-theme-surface border border-theme-border rounded-lg p-3">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __(':count posts selected', ['count' => count($selectedPostIds)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,275 @@
|
||||
{{-- Mailing Report Modal Partial --}}
|
||||
@php
|
||||
$statusColors = [
|
||||
'draft' => 'bg-gray-100 text-gray-800',
|
||||
'scheduled' => 'bg-yellow-100 text-yellow-800',
|
||||
'sending' => 'bg-blue-100 text-blue-800',
|
||||
'sent' => 'bg-green-100 text-green-800',
|
||||
'aborted' => 'bg-red-100 text-red-800',
|
||||
'cancelled' => 'bg-red-100 text-red-800',
|
||||
];
|
||||
$statusColor = $statusColors[$mailing->status] ?? 'bg-gray-100 text-gray-800';
|
||||
@endphp
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-theme-primary">{{ $mailing->title }}</h3>
|
||||
<p class="text-xs text-theme-secondary mt-0.5">{{ $mailing->id }}</p>
|
||||
@if($mailing->sent_at || $mailing->scheduled_at)
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
@if($mailing->sent_at)
|
||||
{{ __('Sent') }}: {{ $mailing->sent_at->format('d M Y H:i') }}
|
||||
@else
|
||||
{{ __('Scheduled') }}: {{ $mailing->scheduled_at->format('d M Y H:i') }}
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
@if($mailing->sending_started_at)
|
||||
@php
|
||||
$endTime = $mailing->sent_at ?? now();
|
||||
$durationSeconds = (int) $mailing->sending_started_at->diffInSeconds($endTime, true);
|
||||
$durationHours = intdiv($durationSeconds, 3600);
|
||||
$durationMinutes = intdiv($durationSeconds % 3600, 60);
|
||||
$durationSecs = $durationSeconds % 60;
|
||||
$durationStr = $durationHours > 0
|
||||
? sprintf('%dh %dm %ds', $durationHours, $durationMinutes, $durationSecs)
|
||||
: ($durationMinutes > 0
|
||||
? sprintf('%dm %ds', $durationMinutes, $durationSecs)
|
||||
: sprintf('%ds', $durationSecs));
|
||||
@endphp
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
{{ __('Started sending') }}: {{ $mailing->sending_started_at->format('d M Y H:i') }}
|
||||
</p>
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
{{ $mailing->sent_at ? __('Duration') : __('Running for') }}: {{ $durationStr }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<span class="inline-flex px-2.5 py-0.5 text-xs font-medium rounded-full {{ $statusColor }}">
|
||||
{{ __(ucfirst($mailing->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Block 1: Estimated Recipients --}}
|
||||
<div class="p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Recipients by Language & Content') }}</p>
|
||||
|
||||
@php
|
||||
$rowsByLocaleLocation = $mailing->getRecipientCountsByLocaleAndLocation();
|
||||
$fallbackLocale = $mailing->fallback_locale ?? '';
|
||||
@endphp
|
||||
|
||||
@if(count($rowsByLocaleLocation) > 0)
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr class="text-theme-secondary font-medium">
|
||||
<th style="text-align: left; padding: 0 8px 4px 0; font-weight: 500;">{{ __('Language') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500;">{{ __('Posts') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500; white-space: nowrap;">{{ __('Location') }}</th>
|
||||
<th style="text-align: right; padding: 0 0.5rem 4px 8px; font-weight: 500;">{{ __('Recipients') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$prevLocale = null;
|
||||
$fallbackLanguageName = \App\Models\Language::where('lang_code', $fallbackLocale)->value('name') ?? strtoupper($fallbackLocale);
|
||||
@endphp
|
||||
@foreach($rowsByLocaleLocation as $row)
|
||||
@php
|
||||
$isFirstForLocale = $row['locale'] !== $prevLocale;
|
||||
$prevLocale = $row['locale'];
|
||||
$language = $isFirstForLocale ? \App\Models\Language::where('lang_code', $row['locale'])->first() : null;
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
$languageName = $language ? $language->name : strtoupper($row['locale']);
|
||||
$nativePosts = $row['content_blocks'] - ($row['fallback_posts'] ?? 0);
|
||||
$fallbackPosts = $row['fallback_posts'] ?? 0;
|
||||
$missingPosts = $row['missing_posts'] ?? 0;
|
||||
@endphp
|
||||
<tr>
|
||||
<td style="padding: 2px 8px 2px 0; white-space: nowrap;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>{{ $flag }}</span>
|
||||
<span class="font-medium text-theme-primary">{{ $languageName }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="padding: 2px 8px;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
@if($nativePosts > 0)
|
||||
<span class="font-medium rounded bg-green-100 text-green-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $nativePosts }} {{ __('native') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($fallbackPosts > 0)
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $fallbackPosts }} {{ __('fallback in') }} {{ $fallbackLanguageName }}
|
||||
</span>
|
||||
@endif
|
||||
@if($missingPosts > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $missingPosts }} {{ __('skipped') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($nativePosts === 0 && $fallbackPosts === 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ __('No content') }}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-theme-secondary" style="padding: 2px 8px; white-space: nowrap;">
|
||||
{{ $row['location'] ?: __('Unknown') }}
|
||||
</td>
|
||||
<td class="text-theme-secondary" style="padding: 2px 0.5rem 2px 8px; text-align: right;">
|
||||
{{ number_format($row['count']) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@php
|
||||
$totalEstimated = collect($rowsByLocaleLocation)->sum('count');
|
||||
$suppressedCount = $mailing->getSuppressedRecipientsCount();
|
||||
@endphp
|
||||
<div class="mt-3 pt-2 border-t border-theme-border space-y-1">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total recipients:') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ number_format($totalEstimated) }}</span>
|
||||
</div>
|
||||
@if($suppressedCount > 0)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-theme-secondary">{{ __('Skipped (hard bounce):') }}</span>
|
||||
<span class="font-medium text-gray-700" style="padding-right: 0.5rem;">{{ number_format($suppressedCount) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-theme-secondary">{{ __('No recipients found for this mailing type.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Block 2: Send Results (only shown once sending has started) --}}
|
||||
@if(in_array($mailing->status, ['sending', 'sent', 'aborted']))
|
||||
@php
|
||||
$bounceStats = $this->getReportBounceStats();
|
||||
$recipientsCount = $mailing->recipients_count ?: 1;
|
||||
$processedCount = $mailing->sent_count + $mailing->failed_count + $mailing->skipped_count + $mailing->aborted_count;
|
||||
$progressPct = min(100, round($processedCount / $recipientsCount * 100));
|
||||
@endphp
|
||||
|
||||
<div class="p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Send Results') }}</p>
|
||||
|
||||
{{-- Progress bar (while sending) --}}
|
||||
@if($mailing->status === 'sending')
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-xs text-theme-secondary mb-1">
|
||||
<span>{{ __('Progress') }}</span>
|
||||
<span>{{ number_format($processedCount) }} / {{ number_format($mailing->recipients_count) }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-theme-brand h-2 rounded-full transition-all" style="width: {{ $progressPct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Results table --}}
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Successfully sent') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;" class="text-theme-secondary">
|
||||
{{ number_format($mailing->sent_count) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Failed (delivery error)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
@if($mailing->failed_count > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->failed_count) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-theme-secondary">0</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if($mailing->skipped_count > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">
|
||||
{{ __('Skipped (hard bounce)') }}
|
||||
</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-gray-100 text-gray-700" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->skipped_count) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($mailing->aborted_count > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Not sent (aborted)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-red-100 text-red-800" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->aborted_count) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['hard'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Bounced (hard)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-red-100 text-red-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['hard']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['soft'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Bounced (soft)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['soft']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['complaint'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Complaints') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-orange-100 text-orange-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['complaint']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Totals --}}
|
||||
<div class="mt-3 pt-2 border-t border-theme-border space-y-1">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total sent to:') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ number_format($mailing->recipients_count) }}</span>
|
||||
</div>
|
||||
@if($mailing->sent_count > 0 && $mailing->recipients_count > 0)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-theme-secondary">{{ __('Success rate') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ round($mailing->sent_count / $mailing->recipients_count * 100, 1) }}%</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
Reference in new issue
Block a user