Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,385 @@
<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