386 lines
21 KiB
PHP
386 lines
21 KiB
PHP
<div class="my-4">
|
|
<style>
|
|
/* Override WireUI select - nuclear approach to eliminate all red/negative colors */
|
|
#content-1 [x-data*="wireui_select"] *,
|
|
#content-1 [x-data*="wireui_select"] *:focus,
|
|
#content-1 [x-data*="wireui_select"] *:focus-within {
|
|
border-color: rgb(var(--color-accent)) !important;
|
|
--tw-border-opacity: 1 !important;
|
|
}
|
|
|
|
/* Specifically override negative color classes */
|
|
#content-1 .border-negative-300,
|
|
#content-1 .border-negative-400,
|
|
#content-1 .border-negative-500,
|
|
#content-1 .focus\:border-negative-300:focus,
|
|
#content-1 .focus\:border-negative-400:focus,
|
|
#content-1 .focus\:border-negative-500:focus,
|
|
#content-1 [class*="negative"],
|
|
#content-1 [style*="border"] {
|
|
border-color: rgb(var(--color-accent)) !important;
|
|
}
|
|
|
|
/* Override rings */
|
|
#content-1 [x-data*="wireui_select"] *[class*="ring"] {
|
|
--tw-ring-color: rgb(var(--color-accent)) !important;
|
|
--tw-ring-opacity: 1 !important;
|
|
}
|
|
</style>
|
|
|
|
<!-- Accordion Item -->
|
|
<div class="border-theme-primary rounded-md border px-6 py-3 shadow-md">
|
|
<button class="flex w-full items-center justify-between py-2" onclick="toggleAccordion(1)">
|
|
<span class="text-theme-primary text-xs font-semibold uppercase tracking-widest">
|
|
{{ __('Search transactions') }}</span>
|
|
<span class="transition-transform duration-300" id="icon-1">
|
|
<x-icon class="text-theme-primary hover:text-theme-primary h-5 w-5" name="chevron-down" />
|
|
</span>
|
|
</button>
|
|
<!-- Content of open accordion -->
|
|
<form wire:submit.prevent="getTransactions">
|
|
<div class="max-h-0 transition-all duration-300 ease-in-out" id="content-1" wire:ignore>
|
|
<div class="my-3 flex flex-col md:flex-row md:space-x-12">
|
|
<div class="w-full md:w-2/4 md:flex-none space-y-4 lg:space-y-0">
|
|
<div>
|
|
<x-jetstream.label for="search" value="{{ __('Keywords') }}" />
|
|
<x-jetstream.input :clearable="true" class="text-theme-primary placeholder-theme-light text-sm"
|
|
placeholder="Search keywords" right-icon="search"
|
|
wire:model.defer="search" />
|
|
@error('search')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
@livewire('to-account', ['label' => __('From / to account')])
|
|
@error('account')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
|
|
<div class="lg:my-6 flex flex-col md:flex-row md:items-center">
|
|
<!-- Amount Component -->
|
|
<div class="flex-shrink-0">
|
|
@livewire('amount', [
|
|
'label' => __('Amount'),
|
|
'maxLengthHoursInput' => timebank_config('maxLengthHoursInput.user'),
|
|
])
|
|
{{-- TODO: if user is admin or bank:
|
|
<livewire:amount :label="__('Search amount')" :maxLengthHoursInput="timebank_config('maxLengthHoursInput.bank')">
|
|
--}}
|
|
@error('amount')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
|
|
<!-- Radio Buttons -->
|
|
<div class="mt-4 md:ml-4 md:mt-10 flex items-center space-x-4">
|
|
<x-radio id="credit-debit" label="{{ strtolower(__('Credit/debit')) }}" value=""
|
|
wire:model="amountType" />
|
|
<x-radio id="credit" label="{{ strtolower(__('Credit')) }}" value="credit"
|
|
wire:model="amountType" />
|
|
<x-radio id="debit" label="{{ strtolower(__('Debit')) }}" value="debit"
|
|
wire:model="amountType" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="z-50 mt-4 lg:my-3 xl:my-0 w-full md:flex-auto">
|
|
<label class="mb-1 block text-sm font-medium text-gray-700">{{ __('From date') }}</label>
|
|
<x-flatpickr altFormat="d-m-Y"
|
|
class="placeholder-theme-light mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-theme-accent focus:ring-theme-accent"
|
|
dateFormat="Y-m-d" placeholder="{{ __('Select a date') }}"
|
|
wire:model.defer="fromDate" />
|
|
</div>
|
|
<div class="z-50 mt-4 lg:my-3 xl:my-0 w-full md:flex-auto">
|
|
<label class="mb-1 block text-sm font-medium text-gray-700">{{ __('To date') }}</label>
|
|
<x-flatpickr altFormat="d-m-Y"
|
|
class="placeholder-theme-light mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-theme-accent focus:ring-theme-accent"
|
|
dateFormat="Y-m-d" placeholder="{{ __('Select a date') }}"
|
|
wire:model.defer="toDate" />
|
|
</div>
|
|
</div>
|
|
|
|
<!--- Transaction type --->
|
|
<div class="w-full md:w-2/4 md:flex-none mt-4 lg:mt-0">
|
|
<x-jetstream.label for="searchTypes" value="{{ __('Transaction types') }}" />
|
|
<x-select :options="$typeOptions" multiselect option-label="name" option-value="id"
|
|
placeholder="{{ __('Select (multiple) types') }}" wire:model="searchTypes" />
|
|
</div>
|
|
@error('searchTypes')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
<div class="py-6">
|
|
<x-jetstream.secondary-button class="my-3" type="submit">
|
|
{{ __('Search') }}
|
|
</x-jetstream.secondary-button>
|
|
<x-jetstream.secondary-button class="my-3 ml-4" wire:click.prevent="resetSearch">
|
|
{{ __('Clear all') }}
|
|
</x-jetstream.secondary-button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- General error section -->
|
|
@if (session('error'))
|
|
<div class="relative mt-6 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700" role="alert">
|
|
<strong class="font-bold">Error!</strong>
|
|
<span class="block sm:inline">{{ session('error') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Results table -->
|
|
<div class="relative mb-6 mt-6 flex items-center justify-between">
|
|
<div wire:loading>
|
|
<x-mini-button flat icon="" primary rounded spinner /> <span> {{ __('Loading...') }} </span>
|
|
</div>
|
|
<div class="ml-auto hidden md:flex md:items-center md:space-x-2">
|
|
{{-- TODO: Add pdf /html export for prints --}}
|
|
<x-jetstream.secondary-button wire:click="exportTransactions('ods')">
|
|
<x-icon class="mr-1 h-4 w-4" name="arrow-down-tray" />
|
|
{{ __('ODS') }}
|
|
</x-jetstream.secondary-button>
|
|
<x-jetstream.secondary-button wire:click="exportTransactions('xlsx')">
|
|
<x-icon class="mr-1 h-4 w-4" name="arrow-down-tray" />
|
|
{{ __('XLSX') }}
|
|
</x-jetstream.secondary-button>
|
|
<x-jetstream.secondary-button wire:click="exportTransactions('csv')">
|
|
<x-icon class="mr-1 h-4 w-4" name="arrow-down-tray" />
|
|
{{ __('CSV') }}
|
|
</x-jetstream.secondary-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200" id="transactions">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
|
|
{{ __('Date') }}
|
|
</th>
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
|
|
{{ __('From / to') }}
|
|
</th>
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle hidden md:table-cell">
|
|
{{ __('Details') }}
|
|
</th>
|
|
<th class="px-3 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
|
|
{{ __('Amount') }}
|
|
</th>
|
|
@if ($hideBalance === false)
|
|
<th class="px-3 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider align-middle hidden md:table-cell">
|
|
{{ __('Balance') }}
|
|
</th>
|
|
@endif
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@if ($transactions)
|
|
@foreach ($transactions as $transaction)
|
|
<tr onclick="window.location='{{ route('transaction.show', $transaction['trans_id']) }}'"
|
|
class="cursor-pointer hover:bg-gray-50">
|
|
<td class="px-3 py-4 text-sm text-gray-500">
|
|
<div class="leading-tight">
|
|
<div>{{ \Carbon\Carbon::parse($transaction['datetime'])->translatedFormat('d M') }}</div>
|
|
<div>{{ \Carbon\Carbon::parse($transaction['datetime'])->translatedFormat('Y') }}</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-3 py-4 text-sm w-1/5 md:w-1/3 lg:w-1/5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<img alt="profile"
|
|
class="h-8 w-8 md:h-10 md:w-10 lg:h-12 lg:w-12 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600"
|
|
src="{{ Storage::url($transaction['profile_photo']) }}" />
|
|
</div>
|
|
<div class="ml-3 min-w-0 flex-1">
|
|
<div class="text-sm font-medium text-gray-900">
|
|
<div class="block lg:hidden max-w-[100px] sm:max-w-none">
|
|
{{ $transaction['c/d'] === 'Debit' ? __('To') : __('From') }}
|
|
<div class="truncate">{{ $transaction['relation'] }}</div>
|
|
</div>
|
|
<div class="hidden lg:block">
|
|
<div class="truncate">
|
|
{{ $transaction['c/d'] === 'Debit' ? str_replace('@NAME@', $transaction['relation'], __('To @NAME@')) : str_replace('@NAME@', $transaction['relation'], __('From @NAME@')) }}
|
|
</div>
|
|
@if ($transaction['relation_full_name'] == $transaction['relation'])
|
|
<div class="text-xs text-gray-500 font-normal truncate">
|
|
{{ $transaction['relation_location'] }}
|
|
</div>
|
|
@else
|
|
<div class="text-xs text-gray-500 font-normal truncate">
|
|
{{ $transaction['relation_full_name'] }}, {{ $transaction['relation_location'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="text-xs text-gray-500 hidden lg:block truncate">
|
|
@if (isset($transaction['account_to_name']))
|
|
{{ __(ucfirst(strtolower($transaction['account_to_name']))) }}
|
|
{{ __('account') }}
|
|
@else
|
|
{{ __(ucfirst(strtolower($transaction['account_from_name']))) }}
|
|
{{ __('account') }}
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-3 py-4 text-sm text-gray-500 hidden md:table-cell">
|
|
@if ($hideBalance === false)
|
|
{{ strlen($transaction['description']) > 63 ? substr_replace($transaction['description'], '...', 60) : $transaction['description'] }}
|
|
@else
|
|
{{ strlen($transaction['description']) > 83 ? substr_replace($transaction['description'], '...', 80) : $transaction['description'] }}
|
|
@endif
|
|
</td>
|
|
<td class="px-3 py-4 whitespace-nowrap text-right text-sm">
|
|
@if ($transaction['c/d'] === 'Debit')
|
|
<span class="text-red-700 font-medium">{{ tbFormat($transaction['amount']) }} -</span>
|
|
@else
|
|
<span class="text-gray-900 font-medium">{{ tbFormat($transaction['amount']) }} +</span>
|
|
@endif
|
|
</td>
|
|
@if ($hideBalance === false)
|
|
<td class="px-3 py-4 whitespace-nowrap text-right text-sm hidden md:table-cell">
|
|
@if ($transaction['balance'] < 0)
|
|
<span class="text-red-700 font-medium">{{ tbFormat($transaction['balance']) }}</span>
|
|
@else
|
|
<span class="text-gray-900 font-medium">{{ tbFormat($transaction['balance']) }}</span>
|
|
@endif
|
|
</td>
|
|
@endif
|
|
@endforeach
|
|
@else
|
|
<tr>
|
|
<td colspan="5" class="px-6 py-12 text-center text-gray-500">
|
|
<span wire:loading>
|
|
{{ __('One moment, collecting all your transactions...') }}
|
|
</span>
|
|
<span wire:loading.remove>
|
|
{{ __('No transactions found') }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Pagination -->
|
|
@if($transactions && $transactions->hasPages())
|
|
<div class="px-6 py-3 border-t border-gray-200">
|
|
{{ $transactions->links('livewire.long-paginator') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Results count and per-page selector -->
|
|
@if ($transactions && $transactions->total() > 0)
|
|
<div class="mt-4 flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<select class="w-20 rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
|
wire:model.live="perPage">
|
|
<option value="15">15</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
<span class="ml-2 text-sm text-gray-500">{{ __('per page') }}</span>
|
|
</div>
|
|
|
|
<div class="text-sm text-gray-500">
|
|
{{ trans_choice('messages.transactions_found', $transactions->total(), ['count' => $transactions->total()]) }}
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@push('scripts')
|
|
{{-- Accordion script --}}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize all accordions to be collapsed by default
|
|
const accordions = document.querySelectorAll('[id^="content-"]');
|
|
accordions.forEach(content => {
|
|
content.style.maxHeight = '0';
|
|
content.style.overflow = 'hidden';
|
|
});
|
|
|
|
const icons = document.querySelectorAll('[id^="icon-"]');
|
|
icons.forEach(icon => {
|
|
icon.innerHTML = `
|
|
<x-icon class="h-4 w-4 text-theme-primary hover:text-theme-primary font-bold" name="chevron-down" />
|
|
`;
|
|
});
|
|
});
|
|
|
|
function toggleAccordion(index) {
|
|
const content = document.getElementById(`content-${index}`);
|
|
const icon = document.getElementById(`icon-${index}`);
|
|
const toggleButton = document.getElementById(`toggle-button-${index}`);
|
|
|
|
// SVG for Down icon
|
|
const downSVG = `
|
|
<x-icon class="h-4 w-4 text-theme-primary hover:text-theme-primary font-bold" name="chevron-up" />
|
|
`;
|
|
|
|
// SVG for Up icon
|
|
const upSVG = `
|
|
<x-icon class="h-4 w-4 text-theme-primary hover:text-theme-primary font-bold" name="chevron-down" />
|
|
`;
|
|
|
|
// Toggle the content's max-height and overflow for smooth opening and closing
|
|
if (content.style.maxHeight && content.style.maxHeight !== '0px') {
|
|
content.style.maxHeight = '0';
|
|
content.style.overflow = 'hidden';
|
|
icon.innerHTML = upSVG;
|
|
} else {
|
|
content.style.maxHeight = content.scrollHeight + 'px';
|
|
content.style.overflow = 'visible';
|
|
icon.innerHTML = downSVG;
|
|
}
|
|
}
|
|
|
|
// Prevent the accordion from closing when clicking or typing inside it
|
|
document.addEventListener('click', function(event) {
|
|
// Only check if we're on a page with the accordion
|
|
const content = document.getElementById('content-1');
|
|
if (!content) return;
|
|
|
|
const toggleButton = document.getElementById('toggle-button-1');
|
|
if (!toggleButton) return;
|
|
|
|
if (content.contains(event.target) && !toggleButton.contains(event.target)) {
|
|
event.stopPropagation();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('input', function(event) {
|
|
// Only check if we're on a page with the accordion
|
|
const content = document.getElementById('content-1');
|
|
if (!content) return;
|
|
|
|
const toggleButton = document.getElementById('toggle-button-1');
|
|
if (!toggleButton) return;
|
|
|
|
if (content.contains(event.target) && !toggleButton.contains(event.target)) {
|
|
event.stopPropagation();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{{-- Scroll to top when clicking paginator --}}
|
|
<script>
|
|
document.addEventListener('scroll-to-top', event => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|