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,22 @@
|
||||
{{-- Shown on the Post and Questions tabs while the poll's content is read-only (Poll::isContentLocked) --}}
|
||||
@if ($contentLocked)
|
||||
<div class="rounded-md bg-gray-50 border border-gray-300 p-4" role="status">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 text-sm text-gray-700">
|
||||
<p class="font-medium text-gray-800">{{ __('This poll is read-only.') }}</p>
|
||||
<p class="mt-1">
|
||||
@if ($editingPoll?->hasResponses())
|
||||
{{ __('People have already voted, so the post and the questions can no longer change: every voter must see the same poll. Settings and target audience can still be edited.') }}
|
||||
@else
|
||||
{{ __('The poll is no longer a draft, so the post and the questions cannot be changed. To change them, set the status back to Draft and save; that is only possible while nobody has voted.') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,103 @@
|
||||
<div class="space-y-6">
|
||||
@include('livewire.polls.partials.locked-warning')
|
||||
|
||||
<!-- Language of the texts being edited (also usable when read-only) -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm text-gray-700">{{ __('Language') }}:</span>
|
||||
@foreach ($locales as $code)
|
||||
@php($hasTitle = trim((string) ($postFields[$code]['title'] ?? '')) !== '')
|
||||
<button type="button" wire:click="$set('editLocale', '{{ $code }}')"
|
||||
title="{{ $hasTitle ? __('Filled in') : '' }}"
|
||||
class="rounded-md border px-2 py-1 text-xs uppercase {{ $editLocale === $code ? 'border-theme-brand bg-theme-brand text-white' : 'border-theme-border text-theme-primary' }} {{ $hasTitle ? 'font-semibold' : 'opacity-70' }}">
|
||||
{{ $code }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<fieldset @disabled($contentLocked) @class(["space-y-6", "opacity-60" => $contentLocked])>
|
||||
<div wire:key="poll-post-title-{{ $editingPollId }}-{{ $editLocale }}">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Title') }} ({{ strtoupper($editLocale) }})</label>
|
||||
<x-input errorless wire:model.blur="postFields.{{ $editLocale }}.title" type="text" maxlength="150" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "postFields.$editLocale.title"])
|
||||
</div>
|
||||
|
||||
<div wire:key="poll-post-excerpt-{{ $editingPollId }}-{{ $editLocale }}">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Introduction') }} ({{ strtoupper($editLocale) }})</label>
|
||||
<x-textarea errorless wire:model.blur="postFields.{{ $editLocale }}.excerpt" rows="3" maxlength="{{ timebank_config('posts.excerpt_max_input', 500) }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "postFields.$editLocale.excerpt"])
|
||||
</div>
|
||||
|
||||
<!-- Content: WYSIWYG editor (Quill), as in Posts/Manage -->
|
||||
<div wire:ignore wire:key="poll-post-quill-{{ $editingPollId }}-{{ $editLocale }}"
|
||||
x-data="{
|
||||
quill: null,
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.quill = new Quill(this.$refs.editor, {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
['link', 'image', 'video'],
|
||||
['clean']
|
||||
]
|
||||
},
|
||||
theme: 'snow',
|
||||
readOnly: {{ $contentLocked ? 'true' : 'false' }},
|
||||
});
|
||||
this.quill.on('text-change', () => {
|
||||
@this.set('postFields.{{ $editLocale }}.content', this.quill.root.innerHTML, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Content') }} ({{ strtoupper($editLocale) }})</label>
|
||||
<div x-ref="editor">{!! \App\Helpers\StringHelper::sanitizeHtml($postFields[$editLocale]['content'] ?? '') !!}</div>
|
||||
</div>
|
||||
@include('polls.types._error', ['field' => "postFields.$editLocale.content"])
|
||||
|
||||
<!-- Image -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Image') }}</label>
|
||||
@if ($image && !$errors->has('image'))
|
||||
<img class="mb-2 w-64 rounded-md border border-gray-300" src="{{ $image->temporaryUrl() }}" alt="">
|
||||
@elseif ($currentImage)
|
||||
<img class="mb-2 w-64 rounded-md border border-gray-300" src="{{ $currentImage }}" alt="">
|
||||
@else
|
||||
<div class="mb-2 flex h-36 w-64 items-center justify-center border border-gray-300">
|
||||
<span>{{ __('No image') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<div @class(['flex flex-wrap items-center gap-3', 'hidden' => $contentLocked])>
|
||||
<input accept="image/*" type="file" class="hidden" id="poll-image-upload" wire:model="image">
|
||||
<x-jetstream.secondary-button type="button" onclick="document.getElementById('poll-image-upload').click()">
|
||||
{{ __('Browse...') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@if ($image || $currentImage)
|
||||
<x-jetstream.danger-button type="button" wire:click="markImageForRemoval">
|
||||
{{ __('Delete photo') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
<span wire:loading wire:target="image" class="text-sm text-theme-secondary">{{ __('Uploading...') }}</span>
|
||||
</div>
|
||||
@if ($removeImage)
|
||||
<p class="mt-1 text-sm text-theme-secondary">{{ __('The photo will be removed when you save the poll.') }}</p>
|
||||
@endif
|
||||
@include('polls.types._error', ['field' => 'image'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Image ownership') }} ({{ __('all languages') }})</label>
|
||||
<x-input errorless wire:model.blur="mediaOwner" type="text" maxlength="150" class="w-full" />
|
||||
@include('polls.types._error', ['field' => 'mediaOwner'])
|
||||
</div>
|
||||
|
||||
<div wire:key="poll-post-caption-{{ $editingPollId }}-{{ $editLocale }}">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Caption') }} ({{ strtoupper($editLocale) }})</label>
|
||||
<x-textarea errorless wire:model.blur="mediaCaptions.{{ $editLocale }}" rows="2" maxlength="{{ timebank_config('posts.media_caption_max_input', 300) }}" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "mediaCaptions.$editLocale"])
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
<div class="space-y-6">
|
||||
@include('livewire.polls.partials.locked-warning')
|
||||
|
||||
<!-- Language of the texts being edited -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm text-gray-700">{{ __('Language') }}:</span>
|
||||
@foreach ($locales as $code)
|
||||
<button type="button" wire:click="$set('editLocale', '{{ $code }}')"
|
||||
class="rounded-md border px-2 py-1 text-xs uppercase {{ $editLocale === $code ? 'border-theme-brand bg-theme-brand text-white' : 'border-theme-border text-theme-primary' }}">
|
||||
{{ $code }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@forelse ($questions as $index => $question)
|
||||
@php($type = $registry->has($question['type']) ? $registry->get($question['type']) : null)
|
||||
<div class="p-4 bg-theme-surface rounded-md border border-theme-border" wire:key="poll-question-{{ $question['key'] }}">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-4">
|
||||
<span class="text-sm font-semibold text-theme-primary mr-auto">{{ __('Question') }} {{ $index + 1 }}</span>
|
||||
@unless ($contentLocked)
|
||||
<x-jetstream.light-button wire:click="moveQuestion({{ $index }}, -1)" :disabled="$index === 0" title="{{ __('Move up') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-up" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="moveQuestion({{ $index }}, 1)" :disabled="$loop->last" title="{{ __('Move down') }}">
|
||||
<x-icon class="h-4 w-4" name="chevron-down" />
|
||||
</x-jetstream.light-button>
|
||||
<x-jetstream.light-button wire:click="removeQuestion({{ $index }})" title="{{ __('Remove question') }}">
|
||||
<x-icon class="h-4 w-4" name="trash" />
|
||||
</x-jetstream.light-button>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
<fieldset @disabled($contentLocked) @class(["space-y-4", "opacity-60" => $contentLocked])>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Question') }} ({{ strtoupper($editLocale) }})</label>
|
||||
<x-input wire:model="questions.{{ $index }}.label.{{ $editLocale }}" type="text" class="w-full" />
|
||||
@include('polls.types._error', ['field' => "questions.$index.label"])
|
||||
@php($filled = array_keys(array_filter($question['label'] ?? [], fn ($label) => trim((string) $label) !== '')))
|
||||
@if ($filled)
|
||||
<p class="mt-1 text-xs text-theme-secondary">{{ __('Filled in') }}: {{ strtoupper(implode(', ', $filled)) }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Type') }}</label>
|
||||
<select wire:model.live="questions.{{ $index }}.type" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
@foreach ($typeOptions as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
@unless ($type)
|
||||
<option value="{{ $question['type'] }}">{{ $question['type'] }} ({{ __('disabled') }})</option>
|
||||
@endunless
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('polls.types._checkbox', ['model' => "questions.$index.required", 'label' => __('Answer required')])
|
||||
|
||||
@if ($type)
|
||||
@include($type->editorView(), [
|
||||
'type' => $type,
|
||||
'settings' => $question['settings'] ?? [],
|
||||
'prefix' => "questions.$index.settings",
|
||||
'index' => $index,
|
||||
'locale' => $editLocale,
|
||||
])
|
||||
@else
|
||||
<p class="text-sm text-red-600">{{ __('This question type is disabled in the platform configuration.') }}</p>
|
||||
@endif
|
||||
</fieldset>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-theme-secondary">{{ __('No questions yet.') }}</p>
|
||||
@endforelse
|
||||
|
||||
@unless ($contentLocked)
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('New question') }}</label>
|
||||
<select wire:model="newQuestionType" class="border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
@foreach ($typeOptions as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<x-jetstream.secondary-button wire:click="addQuestion">
|
||||
<x-icon class="mr-2 h-4 w-4" name="plus" />
|
||||
{{ __('Add question') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
@endunless
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<div class="space-y-6">
|
||||
@if ($results['may_export'])
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ route('polls.export', $editingPoll) }}"
|
||||
class="inline-flex items-center rounded-md border border-theme-border bg-theme-background px-4 py-2 text-sm text-theme-primary shadow-sm hover:bg-theme-surface">
|
||||
<x-icon class="mr-2 h-5 w-5" name="arrow-down-tray" />
|
||||
{{ __('Download CSV') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Verification state -->
|
||||
<div class="p-3 bg-theme-surface rounded-md border border-theme-border">
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ __('Ballot ledger') }}:</strong>
|
||||
@if ($results['chain']['valid'])
|
||||
{{ __('intact') }} ({{ trans_choice(':count entry|:count entries', $results['chain']['entries'], ['count' => $results['chain']['entries']]) }})
|
||||
@else
|
||||
<span class="text-red-600">{{ __('broken at entry :sequence', ['sequence' => $results['chain']['broken_at']]) }}</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-sm text-theme-secondary mt-1">
|
||||
{{ __('Votes recorded: :votes. Ballots counted in the ledger: :ballots.', ['votes' => $results['response_count'], 'ballots' => $results['active_ballots']]) }}
|
||||
@if ($results['response_count'] !== $results['active_ballots'])
|
||||
<span class="text-red-600">{{ __('These numbers should be equal.') }}</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Results per question -->
|
||||
@foreach ($results['questions'] as $question)
|
||||
@if ($registry->has($question->type))
|
||||
@php($type = $registry->get($question->type))
|
||||
<div wire:key="poll-admin-results-{{ $question->id }}">
|
||||
<p class="mb-2 text-sm font-semibold text-theme-primary">{{ $question->labelFor($locale) }}</p>
|
||||
@include($type->resultsView(), [
|
||||
'type' => $type,
|
||||
'question' => $question,
|
||||
'settings' => $question->settings ?? [],
|
||||
'summary' => $results['summaries'][$question->id],
|
||||
'locale' => $locale,
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<!-- Voters -->
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-semibold text-theme-primary">{{ __('Voters') }}</p>
|
||||
@if ($editingPoll->anonymous)
|
||||
<p class="text-sm text-theme-secondary">{{ __('This poll is anonymous: voters are not shown.') }}</p>
|
||||
@elseif (!$results['may_export'])
|
||||
<p class="text-sm text-theme-secondary">{{ __('Who voted is shown to admins with the poll export permission.') }}</p>
|
||||
@elseif ($results['voters']->isEmpty())
|
||||
<p class="text-sm text-theme-secondary">{{ __('No votes yet.') }}</p>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Profile') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Submitted by') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Voted') }}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Changed') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach ($results['voters'] as $response)
|
||||
<tr wire:key="poll-voter-{{ $response->id }}">
|
||||
<td class="px-3 py-2 text-gray-900">
|
||||
{{ $response->voter?->name ?? __('Deleted profile') }}
|
||||
<span class="text-xs text-gray-500">({{ __(class_basename($response->voter_type)) }})</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-500">{{ $response->user?->name }}</td>
|
||||
<td class="px-3 py-2 text-gray-500 whitespace-nowrap">{{ $response->submitted_at->locale($locale)->isoFormat('lll') }}</td>
|
||||
<td class="px-3 py-2 text-gray-500 whitespace-nowrap">
|
||||
{{ $response->updated_at && $response->updated_at->gt($response->submitted_at->copy()->addSecond()) ? $response->updated_at->locale($locale)->isoFormat('lll') : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if ($results['response_count'] > $results['voters']->count())
|
||||
<p class="mt-2 text-xs text-theme-secondary">{{ __('Showing the latest :count voters.', ['count' => $results['voters']->count()]) }}</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,103 @@
|
||||
@php($hasVotes = (bool) $editingPoll?->hasResponses())
|
||||
@php($sealed = (bool) $editingPoll?->sealed_at)
|
||||
<div class="space-y-6">
|
||||
@if ($sealed)
|
||||
{{-- A sealed result is final: savePoll refuses status and date changes too --}}
|
||||
<p class="text-sm text-theme-secondary">{{ __('This poll is closed and sealed: its result is final, so its status and dates cannot change any more.') }}</p>
|
||||
@endif
|
||||
<fieldset @disabled($sealed) @class(['grid grid-cols-1 gap-4 sm:grid-cols-3', 'opacity-60' => $sealed])>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Status') }}</label>
|
||||
<select wire:model.live="form.status" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
{{-- A poll with votes cannot go back to draft (savePoll refuses it too) --}}
|
||||
<option value="draft" @disabled($editingPoll?->hasResponses())>{{ __('Draft') }}</option>
|
||||
<option value="open">{{ __('Open') }}</option>
|
||||
<option value="closed">{{ __('Closed') }}</option>
|
||||
</select>
|
||||
@include('polls.types._error', ['field' => 'form.status'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Opens (optional)') }}</label>
|
||||
@include('polls.types._date', ['model' => 'form.opens_at', 'key' => 'poll-' . $editingPollId . '-opens_at'])
|
||||
@include('polls.types._error', ['field' => 'form.opens_at'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Closes (optional)') }}</label>
|
||||
@include('polls.types._date', ['model' => 'form.closes_at', 'key' => 'poll-' . $editingPollId . '-closes_at'])
|
||||
@include('polls.types._error', ['field' => 'form.closes_at'])
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@if ($originalStatus === 'draft' && ($form['status'] ?? null) === 'open')
|
||||
<div class="rounded-md bg-gray-50 border border-gray-300 p-4" role="alert">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 text-sm text-gray-700">
|
||||
<p>{{ __('When you save, the poll opens: its post and questions become read-only and the post is published. Check the texts, image and questions first.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php($latePublications = $this->latePublications())
|
||||
@if ($latePublications)
|
||||
<div class="rounded-md bg-gray-50 border border-gray-300 p-4" role="alert">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 text-sm text-gray-700">
|
||||
<p>{{ __('The post of this poll is published later than the poll opens: :dates. Until then the poll is not shown in that language. Change the publication date on the Posts page.', ['dates' => $this->formatLatePublications($latePublications)]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Votes by organizations, banks and admins') }}</label>
|
||||
{{-- The one-vote rule depends on it, so it is fixed once people voted (savePoll refuses it too) --}}
|
||||
<select wire:model="form.vote_scope" @disabled($hasVotes) class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm disabled:opacity-60">
|
||||
<option value="per_profile">{{ __('One vote per profile') }}</option>
|
||||
<option value="per_user">{{ __('One vote per linked user') }}</option>
|
||||
</select>
|
||||
@include('polls.types._error', ['field' => 'form.vote_scope'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Who may vote for an organization or bank') }}</label>
|
||||
<select wire:model="form.member_scope" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
<option value="all_users">{{ __('All linked users') }}</option>
|
||||
<option value="managers_coordinators">{{ __('Only managers and coordinators') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Results visible to voters') }}</label>
|
||||
<select wire:model="form.results_visibility" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
<option value="never">{{ __('Never') }}</option>
|
||||
<option value="after_vote">{{ __('After voting') }}</option>
|
||||
<option value="after_close">{{ __('After the poll closes') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Verification') }}</label>
|
||||
<select wire:model="form.verification" class="w-full border border-primary-300 bg-theme-background rounded-md shadow-sm text-sm">
|
||||
<option value="public">{{ __('Public: receipts and anonymous ballot list') }}</option>
|
||||
<option value="receipts">{{ __('Receipts only') }}</option>
|
||||
<option value="off">{{ __('Off') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@include('polls.types._checkbox', ['model' => 'form.allow_vote_change', 'label' => __('Voters may change their vote until the poll closes')])
|
||||
{{-- Anonymity was promised to the voters: it can be switched on, never off, once people voted --}}
|
||||
@include('polls.types._checkbox', ['model' => 'form.anonymous', 'label' => __('Anonymous: hide who voted in the results (votes are still recorded once per voter)'), 'disabled' => $hasVotes && $editingPoll?->anonymous])
|
||||
@include('polls.types._error', ['field' => 'form.anonymous'])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
@php($isOpenNow = $poll->isOpen())
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full
|
||||
@if ($poll->status === 'draft') bg-gray-100 text-gray-800
|
||||
@elseif ($poll->status === 'open' && $isOpenNow) bg-green-100 text-green-800
|
||||
@elseif ($poll->status === 'open') bg-yellow-100 text-yellow-800
|
||||
@else bg-blue-100 text-blue-800 @endif">
|
||||
@if ($poll->status === 'draft')
|
||||
{{ __('Draft') }}
|
||||
@elseif ($poll->status === 'open' && $isOpenNow)
|
||||
{{ __('Open') }}
|
||||
@elseif ($poll->status === 'open' && $poll->opens_at?->isFuture())
|
||||
{{ __('Scheduled') }}
|
||||
@elseif ($poll->status === 'open')
|
||||
{{ __('Ended') }}
|
||||
@else
|
||||
{{ __('Closed') }}
|
||||
@endif
|
||||
</span>
|
||||
@@ -0,0 +1,66 @@
|
||||
<div class="space-y-6">
|
||||
<p class="text-sm text-theme-secondary">{{ __('Leave everything empty to show the poll to all profiles. Each filter you set narrows the audience.') }}</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Profile types') }}</label>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
@foreach (['App\Models\User' => __('Users'), 'App\Models\Organization' => __('Organizations'), 'App\Models\Bank' => __('Banks'), 'App\Models\Admin' => __('Admins')] as $class => $label)
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox" wire:model.live="targeting.target_profile_types" value="{{ $class }}"
|
||||
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">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@include('polls.types._checkbox', ['model' => 'targeting.filter_by_location', 'label' => __('Only profiles in a location')])
|
||||
{{-- x-show on $wire: the checkbox is deferred, so a server-side class would not follow it --}}
|
||||
<div class="mt-3" x-data x-show="$wire.targeting.filter_by_location" @style(['display: none' => empty($targeting['filter_by_location'])])>
|
||||
<div wire:ignore>
|
||||
<livewire:mailings.location-filter wire:key="poll-location-filter-{{ $editingPollId }}"
|
||||
:selectedCountries="$targeting['location_country_ids'] ?? []"
|
||||
:selectedDivisions="$targeting['location_division_ids'] ?? []"
|
||||
:selectedCities="$targeting['location_city_ids'] ?? []"
|
||||
:selectedDistricts="$targeting['location_district_ids'] ?? []" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-theme-secondary">{{ __('Only the most specific level you select counts: a city inside a selected country narrows to that city.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Last login from') }}</label>
|
||||
@include('polls.types._date', ['model' => 'targeting.last_login_from', 'key' => 'poll-' . $editingPollId . '-last_login_from', 'withTime' => false])
|
||||
@include('polls.types._error', ['field' => 'targeting.last_login_from'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Last login until') }}</label>
|
||||
@include('polls.types._date', ['model' => 'targeting.last_login_until', 'key' => 'poll-' . $editingPollId . '-last_login_until', 'withTime' => false])
|
||||
@include('polls.types._error', ['field' => 'targeting.last_login_until'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Registered from') }}</label>
|
||||
@include('polls.types._date', ['model' => 'targeting.registered_from', 'key' => 'poll-' . $editingPollId . '-registered_from', 'withTime' => false])
|
||||
@include('polls.types._error', ['field' => 'targeting.registered_from'])
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">{{ __('Registered until') }}</label>
|
||||
@include('polls.types._date', ['model' => 'targeting.registered_until', 'key' => 'poll-' . $editingPollId . '-registered_until', 'withTime' => false])
|
||||
@include('polls.types._error', ['field' => 'targeting.registered_until'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-theme-surface rounded-md border border-theme-border flex flex-wrap items-center gap-3">
|
||||
<x-jetstream.secondary-button wire:click="countMatchingProfiles" wire:loading.attr="disabled" wire:target="countMatchingProfiles">
|
||||
{{ __('Count matching profiles') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@if ($matchingCounts !== null)
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ trans_choice(':count profile|:count profiles', array_sum($matchingCounts), ['count' => number_format(array_sum($matchingCounts))]) }}</strong>
|
||||
({{ collect($matchingCounts)->map(fn ($count, $type) => __($type . 's') . ': ' . number_format($count))->implode(', ') }})
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user