Initial commit
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
<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" placeholder="{{ __('For internal use only, not visible to recipients.') }}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-jetstream.label for="type" value="{{ __('Mailing type') }}" />
|
||||
<x-select id="type" class="mt-1 w-full" wire:model.live="type" placeholder="{{ __('Select a type...') }}">
|
||||
<x-select.option label="{{ __('Local Newsletter') }}" value="local_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter') }}" value="general_newsletter" />
|
||||
<x-select.option label="{{ __('System Message') }}" value="system_message" />
|
||||
</x-select>
|
||||
@if($type)
|
||||
<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 === 'system_message')
|
||||
{{ __('System announcements and important notices') }}
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subject (Multilingual) -->
|
||||
<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>
|
||||
|
||||
@if(count($availableLocales) > 0)
|
||||
<div class="space-y-3">
|
||||
@foreach($availableLocales as $locale)
|
||||
<div class="p-3">
|
||||
<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
|
||||
</div>
|
||||
<x-input
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="{{ __('Subject line in :language', ['language' => strtoupper($locale)]) }}"
|
||||
wire:model.defer="subjects.{{ $locale }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
||||
<x-icon name="globe-europe-africa" class="w-12 h-12 mx-auto text-gray-400 mb-3" />
|
||||
<p class="text-gray-500 mb-2">{{ __('Select posts to see available languages') }}</p>
|
||||
<p class="text-sm text-gray-400">{{ __('Subject fields will appear based on your post translations') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</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 mb-4">{{ __('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>
|
||||
</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>
|
||||
|
||||
<!-- Scheduling -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Scheduling') }}" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="md:w-1/2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Schedule for later') }}</label>
|
||||
<div wire:ignore>
|
||||
<x-flatpickr
|
||||
showTime
|
||||
dateFormat="Y-m-d"
|
||||
timeFormat="H:i"
|
||||
altFormat="d-m-Y @ H:i"
|
||||
altInput
|
||||
placeholder="{{ __('Select a date and time') }}"
|
||||
wire:model.live="scheduledAt"
|
||||
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') }}" />
|
||||
</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>
|
||||
|
||||
<!-- Location Filtering -->
|
||||
@if($type === 'local_newsletter')
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Location Filtering') }}" />
|
||||
</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"
|
||||
/>
|
||||
</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
|
||||
|
||||
<!-- 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">
|
||||
{{ __('messages.mailings.recipients_info.no_type_selected') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<!-- Real-time recipient count -->
|
||||
@if($type)
|
||||
@php
|
||||
$currentRecipientCount = collect($this->getCurrentRecipientCountsByLocale())->sum('count');
|
||||
@endphp
|
||||
@if($currentRecipientCount > 0)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary">
|
||||
{{ __('Current estimated recipients: :count', ['count' => number_format($currentRecipientCount)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-secondary">
|
||||
{{ __('Current estimated recipients: 0') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($type)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">
|
||||
{{ __('Recipients by Language & Content') }}
|
||||
</p>
|
||||
|
||||
@php
|
||||
$countsByLocale = $this->getCurrentRecipientCountsByLocale();
|
||||
@endphp
|
||||
|
||||
@if(count($countsByLocale) > 0)
|
||||
<div class="space-y-2">
|
||||
@foreach($countsByLocale as $locale => $data)
|
||||
@php
|
||||
$language = \App\Models\Language::where('lang_code', $locale)->first();
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
$languageName = $language ? $language->name : strtoupper($locale);
|
||||
@endphp
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>{{ $flag }}</span>
|
||||
<span class="font-medium">{{ $languageName }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-theme-secondary">
|
||||
{{ number_format($data['count']) }} {{ __('recipients') }}
|
||||
</span>
|
||||
@if($data['has_content'])
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">
|
||||
{{ $data['content_blocks'] }} {{ __('posts') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-theme-danger-light text-theme-danger-dark">
|
||||
{{ __('No content') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalRecipients = collect($countsByLocale)->sum('count');
|
||||
$effectiveRecipients = collect($countsByLocale)->where('has_content', true)->sum('count');
|
||||
@endphp
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-theme-border">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total recipients:') }}</span>
|
||||
<span class="text-theme-secondary">{{ number_format($totalRecipients) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs mt-1">
|
||||
<span class="font-medium text-theme-primary">{{ __('Will receive mailing:') }}</span>
|
||||
<span class="text-theme-secondary font-medium">{{ number_format($effectiveRecipients) }}</span>
|
||||
</div>
|
||||
@if($effectiveRecipients != $totalRecipients)
|
||||
<p class="text-xs text-theme-secondary mt-2">
|
||||
{{ __('Some recipients will not receive the mailing due to missing content in their language.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-theme-secondary">{{ __('No recipients found for this mailing type.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,116 @@
|
||||
<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->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)
|
||||
<span>{{ $post->category->translations->first()->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;
|
||||
$categoryName = $post->category->translations->first()->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>
|
||||
Reference in New Issue
Block a user