289 lines
11 KiB
PHP
289 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\ConsultationType;
|
|
use App\Enums\PaymentStatus;
|
|
use App\Models\Consultation;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use League\Csv\Writer;
|
|
use Livewire\Volt\Component;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
new class extends Component {
|
|
public string $consultationType = 'all';
|
|
public string $status = 'all';
|
|
public string $paymentStatus = 'all';
|
|
public string $dateFrom = '';
|
|
public string $dateTo = '';
|
|
|
|
public function exportCsv(): ?StreamedResponse
|
|
{
|
|
$count = $this->getFilteredConsultations()->count();
|
|
|
|
if ($count === 0) {
|
|
$this->dispatch('notify', type: 'info', message: __('export.no_consultations_match'));
|
|
|
|
return null;
|
|
}
|
|
|
|
$locale = auth()->user()->preferred_language ?? 'ar';
|
|
|
|
return response()->streamDownload(function () use ($locale) {
|
|
// UTF-8 BOM for Excel Arabic support
|
|
echo "\xEF\xBB\xBF";
|
|
|
|
$csv = Writer::createFromString();
|
|
|
|
// Headers based on admin language
|
|
$csv->insertOne([
|
|
__('export.client_name', [], $locale),
|
|
__('export.date', [], $locale),
|
|
__('export.time', [], $locale),
|
|
__('export.consultation_type', [], $locale),
|
|
__('export.status', [], $locale),
|
|
__('export.payment_status', [], $locale),
|
|
__('export.problem_summary', [], $locale),
|
|
]);
|
|
|
|
$this->getFilteredConsultations()->cursor()->each(function ($consultation) use ($csv, $locale) {
|
|
$csv->insertOne([
|
|
$consultation->user->full_name,
|
|
$consultation->booking_date->format('Y-m-d'),
|
|
$consultation->booking_time,
|
|
__('export.type_'.$consultation->consultation_type->value, [], $locale),
|
|
__('export.status_'.$consultation->status->value, [], $locale),
|
|
$this->getPaymentStatusLabel($consultation->payment_status, $locale),
|
|
$consultation->problem_summary,
|
|
]);
|
|
});
|
|
|
|
echo $csv->toString();
|
|
}, 'consultations-export-'.now()->format('Y-m-d').'.csv', [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
]);
|
|
}
|
|
|
|
public function exportPdf(): ?StreamedResponse
|
|
{
|
|
$consultations = $this->getFilteredConsultations()->get();
|
|
|
|
if ($consultations->isEmpty()) {
|
|
$this->dispatch('notify', type: 'info', message: __('export.no_consultations_match'));
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($consultations->count() > 500) {
|
|
$this->dispatch('notify', type: 'warning', message: __('export.large_export_warning'));
|
|
}
|
|
|
|
$locale = auth()->user()->preferred_language ?? 'ar';
|
|
|
|
$pdf = Pdf::loadView('pdf.consultations-export', [
|
|
'consultations' => $consultations,
|
|
'locale' => $locale,
|
|
'generatedAt' => now(),
|
|
'filters' => $this->getActiveFilters(),
|
|
'totalCount' => $consultations->count(),
|
|
]);
|
|
|
|
$pdf->setOption('isHtml5ParserEnabled', true);
|
|
$pdf->setOption('defaultFont', 'DejaVu Sans');
|
|
|
|
return response()->streamDownload(
|
|
fn () => print($pdf->output()),
|
|
'consultations-export-'.now()->format('Y-m-d').'.pdf'
|
|
);
|
|
}
|
|
|
|
public function clearFilters(): void
|
|
{
|
|
$this->consultationType = 'all';
|
|
$this->status = 'all';
|
|
$this->paymentStatus = 'all';
|
|
$this->dateFrom = '';
|
|
$this->dateTo = '';
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'consultationTypes' => [
|
|
'all' => __('export.all_consultation_types'),
|
|
'free' => __('export.type_free'),
|
|
'paid' => __('export.type_paid'),
|
|
],
|
|
'statuses' => [
|
|
'all' => __('export.all_statuses'),
|
|
'pending' => __('export.status_pending'),
|
|
'approved' => __('export.status_approved'),
|
|
'completed' => __('export.status_completed'),
|
|
'cancelled' => __('export.status_cancelled'),
|
|
'no_show' => __('export.status_no_show'),
|
|
'rejected' => __('export.status_rejected'),
|
|
],
|
|
'paymentStatuses' => [
|
|
'all' => __('export.all_payment_statuses'),
|
|
'pending' => __('export.payment_pending'),
|
|
'received' => __('export.payment_received'),
|
|
'na' => __('export.payment_not_applicable'),
|
|
],
|
|
'previewCount' => $this->getFilteredConsultations()->count(),
|
|
];
|
|
}
|
|
|
|
private function getFilteredConsultations()
|
|
{
|
|
return Consultation::query()
|
|
->with('user:id,full_name')
|
|
->when($this->consultationType !== 'all', fn ($q) => $q->where('consultation_type', $this->consultationType))
|
|
->when($this->status !== 'all', fn ($q) => $q->where('status', $this->status))
|
|
->when($this->paymentStatus !== 'all', fn ($q) => $q->where('payment_status', $this->paymentStatus))
|
|
->when($this->dateFrom, fn ($q) => $q->whereDate('booking_date', '>=', $this->dateFrom))
|
|
->when($this->dateTo, fn ($q) => $q->whereDate('booking_date', '<=', $this->dateTo))
|
|
->orderBy('booking_date', 'desc');
|
|
}
|
|
|
|
private function getActiveFilters(): array
|
|
{
|
|
$filters = [];
|
|
|
|
if ($this->consultationType !== 'all') {
|
|
$filters['consultation_type'] = __('export.type_'.$this->consultationType);
|
|
}
|
|
|
|
if ($this->status !== 'all') {
|
|
$filters['status'] = __('export.status_'.$this->status);
|
|
}
|
|
|
|
if ($this->paymentStatus !== 'all') {
|
|
$filters['payment_status'] = $this->getPaymentStatusLabel(PaymentStatus::from($this->paymentStatus));
|
|
}
|
|
|
|
if ($this->dateFrom) {
|
|
$filters['date_from'] = $this->dateFrom;
|
|
}
|
|
|
|
if ($this->dateTo) {
|
|
$filters['date_to'] = $this->dateTo;
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
private function getPaymentStatusLabel(PaymentStatus $status, ?string $locale = null): string
|
|
{
|
|
return match ($status) {
|
|
PaymentStatus::Pending => __('export.payment_pending', [], $locale),
|
|
PaymentStatus::Received => __('export.payment_received', [], $locale),
|
|
PaymentStatus::NotApplicable => __('export.payment_not_applicable', [], $locale),
|
|
};
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<flux:heading size="xl">{{ __('export.export_consultations') }}</flux:heading>
|
|
<flux:text class="mt-1 text-zinc-500 dark:text-zinc-400">{{ __('export.export_consultations_description') }}</flux:text>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6 rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<flux:heading size="lg" class="mb-4">{{ __('export.filters_applied') }}</flux:heading>
|
|
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
|
<div>
|
|
<flux:select wire:model.live="consultationType" :label="__('export.consultation_type')">
|
|
@foreach ($consultationTypes as $value => $label)
|
|
<flux:select.option value="{{ $value }}">{{ $label }}</flux:select.option>
|
|
@endforeach
|
|
</flux:select>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:select wire:model.live="status" :label="__('export.status')">
|
|
@foreach ($statuses as $value => $label)
|
|
<flux:select.option value="{{ $value }}">{{ $label }}</flux:select.option>
|
|
@endforeach
|
|
</flux:select>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:select wire:model.live="paymentStatus" :label="__('export.payment_status')">
|
|
@foreach ($paymentStatuses as $value => $label)
|
|
<flux:select.option value="{{ $value }}">{{ $label }}</flux:select.option>
|
|
@endforeach
|
|
</flux:select>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:input
|
|
wire:model.live="dateFrom"
|
|
type="date"
|
|
:label="__('export.date_from')"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:input
|
|
wire:model.live="dateTo"
|
|
type="date"
|
|
:label="__('export.date_to')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($consultationType !== 'all' || $status !== 'all' || $paymentStatus !== 'all' || $dateFrom || $dateTo)
|
|
<div class="mt-4">
|
|
<flux:button wire:click="clearFilters" variant="ghost" icon="x-mark" size="sm">
|
|
{{ __('export.clear_filters') }}
|
|
</flux:button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
|
<div>
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">
|
|
{{ __('export.total_records') }}: <span class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $previewCount }}</span>
|
|
</flux:text>
|
|
</div>
|
|
|
|
<div class="flex gap-3">
|
|
<flux:button
|
|
wire:click="exportCsv"
|
|
wire:loading.attr="disabled"
|
|
wire:target="exportCsv,exportPdf"
|
|
variant="primary"
|
|
icon="document-arrow-down"
|
|
:disabled="$previewCount === 0"
|
|
>
|
|
<span wire:loading.remove wire:target="exportCsv">{{ __('export.export_csv') }}</span>
|
|
<span wire:loading wire:target="exportCsv">{{ __('export.exporting') }}</span>
|
|
</flux:button>
|
|
|
|
<flux:button
|
|
wire:click="exportPdf"
|
|
wire:loading.attr="disabled"
|
|
wire:target="exportCsv,exportPdf"
|
|
variant="filled"
|
|
icon="document-text"
|
|
:disabled="$previewCount === 0"
|
|
>
|
|
<span wire:loading.remove wire:target="exportPdf">{{ __('export.export_pdf') }}</span>
|
|
<span wire:loading wire:target="exportPdf">{{ __('export.exporting') }}</span>
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($previewCount === 0)
|
|
<div class="mt-6 rounded-lg bg-zinc-50 p-8 text-center dark:bg-zinc-900">
|
|
<flux:icon name="calendar" class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
|
|
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('export.no_consultations_match') }}</flux:text>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|