292 lines
16 KiB
PHP
292 lines
16 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('content')
|
|
<div class="max-w-4xl mx-auto space-y-6">
|
|
<!-- Page Header -->
|
|
<div class="flex items-center gap-4">
|
|
<a href="{{ route('petty-cashes.index') }}" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-900">{{ $pettyCash->title ?? __('pettyCash.pettyCash') }}</h1>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
{{ $pettyCash->status === 'approved' ? 'bg-emerald-50 text-emerald-700' :
|
|
($pettyCash->status === 'rejected' ? 'bg-red-50 text-red-700' :
|
|
($pettyCash->status === 'settled' ? 'bg-blue-50 text-blue-700' : 'bg-amber-50 text-amber-700')) }}">
|
|
{{ $pettyCash->status_label }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Financial Summary -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.amount') }}</p>
|
|
<p class="text-lg font-bold text-gray-900">{{ format_currency((float)$pettyCash->amount, $pettyCash->currency?->code) }}</p>
|
|
@if($pettyCash->original_amount && $pettyCash->original_amount != $pettyCash->amount)
|
|
<p class="text-xs text-gray-400 mt-0.5">{{ __('pettyCash.original_amount') }}: {{ format_currency((float)$pettyCash->original_amount, $pettyCash->currency?->code) }}</p>
|
|
@endif
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.total_expenses') }}</p>
|
|
<p class="text-lg font-bold text-red-600">{{ format_currency((float)$pettyCash->total_expenses, $pettyCash->currency?->code) }}</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.remaining') }}</p>
|
|
<p class="text-lg font-bold {{ $pettyCash->remaining > 0 ? 'text-emerald-600' : ($pettyCash->remaining < 0 ? 'text-red-600' : 'text-gray-900') }}">
|
|
{{ format_currency((float)$pettyCash->remaining, $pettyCash->currency?->code) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Utilization Bar -->
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-sm font-medium text-gray-700">{{ __('pettyCash.utilization') }}</span>
|
|
<span class="text-sm font-bold {{ $pettyCash->utilization >= 90 ? 'text-red-600' : ($pettyCash->utilization >= 70 ? 'text-amber-600' : 'text-emerald-600') }}">{{ $pettyCash->utilization }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-100 rounded-full h-2.5">
|
|
<div class="h-2.5 rounded-full transition-all {{ $pettyCash->utilization >= 90 ? 'bg-red-500' : ($pettyCash->utilization >= 70 ? 'bg-amber-500' : 'bg-emerald-500') }}"
|
|
style="width: {{ min(100, $pettyCash->utilization) }}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail Card -->
|
|
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.project') }}</p>
|
|
<p class="text-sm text-gray-900">{{ $pettyCash->project?->name ?? '-' }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.request_date') }}</p>
|
|
<p class="text-sm text-gray-900">{{ $pettyCash->request_date ? calendar_date($pettyCash->request_date) : '-' }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.requested_by') }}</p>
|
|
<p class="text-sm text-gray-900">{{ $pettyCash->requestedBy?->name ?? '-' }}</p>
|
|
</div>
|
|
@if($pettyCash->approved_by)
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.approved_by') }}</p>
|
|
<p class="text-sm text-gray-900">{{ $pettyCash->approvedBy?->name ?? '-' }}</p>
|
|
</div>
|
|
@endif
|
|
@if($pettyCash->approval_date)
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.approval_date') }}</p>
|
|
<p class="text-sm text-gray-900">{{ calendar_date($pettyCash->approval_date) }}</p>
|
|
</div>
|
|
@endif
|
|
@if($pettyCash->description)
|
|
<div class="md:col-span-2">
|
|
<p class="text-xs font-medium text-gray-500 mb-1">{{ __('pettyCash.description') }}</p>
|
|
<p class="text-sm text-gray-900 whitespace-pre-line">{{ $pettyCash->description }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center gap-3 pt-5 mt-5 border-t border-gray-200">
|
|
@if($pettyCash->status === 'pending')
|
|
<button onclick="approvePC({{ $pettyCash->id }})" class="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg hover:bg-emerald-700 transition-colors">
|
|
{{ __('common.approve') }}
|
|
</button>
|
|
<button onclick="rejectPC({{ $pettyCash->id }})" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors">
|
|
{{ __('common.reject') }}
|
|
</button>
|
|
@elseif($pettyCash->status === 'approved')
|
|
<a href="{{ route('petty-cashes.create-expense', $pettyCash) }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-teal-600 rounded-lg hover:bg-teal-700 transition-colors">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
{{ __('pettyCash.add_expense') }}
|
|
</a>
|
|
<button onclick="confirmSettle({{ $pettyCash->id }})" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
|
|
{{ __('pettyCash.settle') }}
|
|
</button>
|
|
@endif
|
|
@if($pettyCash->status === 'pending')
|
|
<a href="{{ route('petty-cashes.edit', $pettyCash) }}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
|
|
{{ __('common.edit') }}
|
|
</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Expenses -->
|
|
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="text-sm font-bold text-gray-900">{{ __('pettyCash.expenses') }} ({{ $pettyCash->expenses->count() }})</h3>
|
|
@if($pettyCash->status === 'approved')
|
|
<a href="{{ route('petty-cashes.create-expense', $pettyCash) }}" class="text-xs font-medium text-teal-700 hover:text-teal-900">
|
|
+ {{ __('pettyCash.add_expense') }}
|
|
</a>
|
|
@endif
|
|
</div>
|
|
@if($pettyCash->expenses->count() > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-5 py-3 text-start text-xs font-medium text-gray-500">{{ __('expense.title') }}</th>
|
|
<th class="px-5 py-3 text-start text-xs font-medium text-gray-500">{{ __('expense.category') }}</th>
|
|
<th class="px-5 py-3 text-start text-xs font-medium text-gray-500">{{ __('expense.amount') }}</th>
|
|
<th class="px-5 py-3 text-start text-xs font-medium text-gray-500">{{ __('expense.date') }}</th>
|
|
<th class="px-5 py-3 text-start text-xs font-medium text-gray-500">{{ __('expense.status') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
@foreach($pettyCash->expenses as $expense)
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-5 py-3 text-sm text-gray-900">{{ $expense->title ?? '-' }}</td>
|
|
<td class="px-5 py-3 text-sm text-gray-600">{{ __('expense.category_' . $expense->category) }}</td>
|
|
<td class="px-5 py-3 text-sm font-medium text-gray-900">{{ format_currency((float)$expense->amount, $expense->currency?->code) }}</td>
|
|
<td class="px-5 py-3 text-sm text-gray-600">{{ $expense->expense_date ? calendar_date($expense->expense_date) : '-' }}</td>
|
|
<td class="px-5 py-3">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
|
{{ $expense->status === 'approved' ? 'bg-emerald-50 text-emerald-700' :
|
|
($expense->status === 'rejected' ? 'bg-red-50 text-red-700' :
|
|
($expense->status === 'paid' ? 'bg-blue-50 text-blue-700' : 'bg-amber-50 text-amber-700')) }}">
|
|
{{ $expense->status_label ?? $expense->status }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="px-5 py-8 text-center text-sm text-gray-400">
|
|
{{ __('pettyCash.no_expenses_yet') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Attachments -->
|
|
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="text-sm font-bold text-gray-900">{{ __('attachment.attachments') }}</h3>
|
|
<label class="cursor-pointer px-3 py-1.5 text-xs font-medium text-teal-700 bg-teal-50 rounded-lg hover:bg-teal-100 transition-colors">
|
|
<svg class="w-4 h-4 inline-block ms-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
{{ __('attachment.add_file') }}
|
|
<input type="file" name="attachment_files[]" multiple accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" class="hidden"
|
|
data-attachable-type="App\Models\PettyCash" data-attachable-id="{{ $pettyCash->id }}"
|
|
onchange="uploadAttachment(this)">
|
|
</label>
|
|
</div>
|
|
<div id="attachments-list">
|
|
@if($pettyCash->attachments->count() > 0)
|
|
<div class="divide-y divide-gray-100">
|
|
@foreach($pettyCash->attachments as $attachment)
|
|
<div class="flex items-center justify-between px-5 py-3 hover:bg-gray-50" id="attachment-{{ $attachment->id }}">
|
|
<div class="flex items-center gap-3">
|
|
@if($attachment->is_image)
|
|
<div class="w-10 h-10 rounded-lg overflow-hidden bg-gray-100 flex-shrink-0">
|
|
<img src="{{ $attachment->url }}" alt="{{ $attachment->file_name }}" class="w-full h-full object-cover">
|
|
</div>
|
|
@elseif($attachment->is_pdf)
|
|
<div class="w-10 h-10 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
|
|
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z"/></svg>
|
|
</div>
|
|
@else
|
|
<div class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center flex-shrink-0">
|
|
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
</div>
|
|
@endif
|
|
<div>
|
|
<a href="{{ route('attachments.download', $attachment) }}" class="text-sm font-medium text-gray-900 hover:text-teal-600">{{ $attachment->file_name }}</a>
|
|
<p class="text-xs text-gray-400">{{ $attachment->formatted_size }}</p>
|
|
</div>
|
|
</div>
|
|
<button onclick="deleteAttachment({{ $attachment->id }})" class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors" title="{{ __('common.delete') }}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
</button>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="px-5 py-8 text-center text-sm text-gray-400">
|
|
{{ __('attachment.no_attachments') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
async function approvePC(id) {
|
|
const res = await fetch(`/petty-cashes/${id}/approve`, {
|
|
method: 'PUT',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }
|
|
});
|
|
if ((await res.json()).success) location.reload();
|
|
}
|
|
async function rejectPC(id) {
|
|
if (!confirm('{{ __("common.areYouSure") }}')) return;
|
|
const res = await fetch(`/petty-cashes/${id}/reject`, {
|
|
method: 'PUT',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }
|
|
});
|
|
if ((await res.json()).success) location.reload();
|
|
}
|
|
|
|
// Settle with confirmation showing remaining
|
|
async function confirmSettle(id) {
|
|
if (!confirm('{{ __("pettyCash.confirm_settle") }}')) return;
|
|
const res = await fetch(`/petty-cashes/${id}/settle`, {
|
|
method: 'PUT',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }
|
|
});
|
|
if ((await res.json()).success) location.reload();
|
|
}
|
|
|
|
// Attachment upload via AJAX
|
|
async function uploadAttachment(input) {
|
|
const files = input.files;
|
|
if (!files.length) return;
|
|
|
|
const formData = new FormData();
|
|
for (let i = 0; i < files.length; i++) {
|
|
formData.append('files[]', files[i]);
|
|
}
|
|
formData.append('attachable_type', input.dataset.attachableType);
|
|
formData.append('attachable_id', input.dataset.attachableId);
|
|
formData.append('category', 'receipt');
|
|
|
|
try {
|
|
const res = await fetch('{{ route("attachments.store") }}', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' },
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert(data.message || '{{ __("attachment.upload_failed") }}');
|
|
}
|
|
} catch (e) {
|
|
alert('{{ __("attachment.upload_failed") }}');
|
|
}
|
|
input.value = '';
|
|
}
|
|
|
|
// Delete attachment via AJAX
|
|
async function deleteAttachment(id) {
|
|
if (!confirm('{{ __("common.areYouSure") }}')) return;
|
|
try {
|
|
const res = await fetch(`/attachments/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
const el = document.getElementById('attachment-' + id);
|
|
if (el) el.remove();
|
|
}
|
|
} catch (e) {
|
|
alert('{{ __("attachment.delete_failed") }}');
|
|
}
|
|
}
|
|
</script>
|
|
@endpush
|
|
@endsection
|