Commit e891ce11 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add file attachment to expenses (proof/receipt upload)

- New migration: attachment_path + attachment_name columns on expenses
- ExpenseForm uses WithFileUploads trait, accepts jpg/png/webp/pdf up to 5MB
- Drag-drop upload zone in the form with preview of attached file name
- Files stored in storage/app/public/expenses/
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent af0946b7
...@@ -26,6 +26,8 @@ class Expense extends Model ...@@ -26,6 +26,8 @@ class Expense extends Model
'receipt_reference', 'receipt_reference',
'expense_date', 'expense_date',
'notes', 'notes',
'attachment_path',
'attachment_name',
'status', 'status',
'cancelled_by', 'cancelled_by',
'cancelled_at', 'cancelled_at',
......
...@@ -9,12 +9,13 @@ ...@@ -9,12 +9,13 @@
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.app')] #[Layout('layouts.app')]
#[Title('تسجيل مصروف')] #[Title('تسجيل مصروف')]
class ExpenseForm extends Component class ExpenseForm extends Component
{ {
use UsesBranchScope; use UsesBranchScope, WithFileUploads;
public string $category = ''; public string $category = '';
public string $amount_display = ''; public string $amount_display = '';
...@@ -24,6 +25,7 @@ class ExpenseForm extends Component ...@@ -24,6 +25,7 @@ class ExpenseForm extends Component
public string $receipt_reference = ''; public string $receipt_reference = '';
public ?string $expense_date = null; public ?string $expense_date = null;
public string $notes = ''; public string $notes = '';
public $attachment = null;
public function mount(): void public function mount(): void
{ {
...@@ -42,6 +44,7 @@ public function rules(): array ...@@ -42,6 +44,7 @@ public function rules(): array
'receipt_reference' => 'nullable|string|max:100', 'receipt_reference' => 'nullable|string|max:100',
'expense_date' => 'required|date', 'expense_date' => 'required|date',
'notes' => 'nullable|string', 'notes' => 'nullable|string',
'attachment' => 'nullable|file|max:5120|mimes:jpg,jpeg,png,pdf,webp',
]; ];
} }
...@@ -55,14 +58,29 @@ public function messages(): array ...@@ -55,14 +58,29 @@ public function messages(): array
'description.max' => 'الوصف يجب ألا يتجاوز 500 حرف', 'description.max' => 'الوصف يجب ألا يتجاوز 500 حرف',
'payment_method.required' => 'اختر طريقة الدفع', 'payment_method.required' => 'اختر طريقة الدفع',
'expense_date.required' => 'تاريخ المصروف مطلوب', 'expense_date.required' => 'تاريخ المصروف مطلوب',
'attachment.max' => 'حجم الملف لا يتجاوز 5 ميجابايت',
'attachment.mimes' => 'الملف يجب أن يكون صورة (jpg, png, webp) أو PDF',
]; ];
} }
public function removeAttachment(): void
{
$this->attachment = null;
}
public function save(ExpenseService $service): void public function save(ExpenseService $service): void
{ {
$this->validate(); $this->validate();
try { try {
$attachmentPath = null;
$attachmentName = null;
if ($this->attachment) {
$attachmentName = $this->attachment->getClientOriginalName();
$attachmentPath = $this->attachment->store('expenses', 'public');
}
$service->recordExpense([ $service->recordExpense([
'branch_id' => $this->getActiveBranchId(), 'branch_id' => $this->getActiveBranchId(),
'category' => $this->category, 'category' => $this->category,
...@@ -73,6 +91,8 @@ public function save(ExpenseService $service): void ...@@ -73,6 +91,8 @@ public function save(ExpenseService $service): void
'receipt_reference' => $this->receipt_reference ?: null, 'receipt_reference' => $this->receipt_reference ?: null,
'expense_date' => $this->expense_date, 'expense_date' => $this->expense_date,
'notes' => $this->notes ?: null, 'notes' => $this->notes ?: null,
'attachment_path' => $attachmentPath,
'attachment_name' => $attachmentName,
], auth()->user()); ], auth()->user());
session()->flash('success', __('تم تسجيل المصروف بنجاح')); session()->flash('success', __('تم تسجيل المصروف بنجاح'));
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('expenses', function (Blueprint $table) {
$table->string('attachment_path', 500)->nullable()->after('notes');
$table->string('attachment_name', 255)->nullable()->after('attachment_path');
});
}
public function down(): void
{
Schema::table('expenses', function (Blueprint $table) {
$table->dropColumn(['attachment_path', 'attachment_name']);
});
}
};
...@@ -127,6 +127,33 @@ class="w-full rounded-lg border-gray-300 text-sm py-2.5"> ...@@ -127,6 +127,33 @@ class="w-full rounded-lg border-gray-300 text-sm py-2.5">
<textarea wire:model="notes" rows="2" <textarea wire:model="notes" rows="2"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"></textarea> class="w-full rounded-lg border-gray-300 text-sm py-2.5"></textarea>
</div> </div>
{{-- Attachment --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('مرفق (إيصال / إثبات)') }}</label>
@if($attachment)
<div class="flex items-center gap-3 p-3 bg-green-50 border border-green-200 rounded-lg mb-2">
<svg class="w-5 h-5 text-green-600 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="text-sm text-green-700 truncate">{{ $attachment->getClientOriginalName() }}</span>
<button type="button" wire:click="removeAttachment" class="ms-auto text-red-500 hover:text-red-700 text-xs">
{{ __('إزالة') }}
</button>
</div>
@endif
<label class="flex flex-col items-center justify-center w-full h-28 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-amber-400 hover:bg-amber-50/50 transition-colors">
<div class="flex flex-col items-center">
<svg class="w-8 h-8 text-gray-400 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="text-xs text-gray-500">{{ __('اضغط لرفع صورة أو ملف PDF (حد أقصى 5MB)') }}</span>
</div>
<input type="file" wire:model="attachment" class="hidden" accept=".jpg,.jpeg,.png,.webp,.pdf">
</label>
<div wire:loading wire:target="attachment" class="mt-1 text-xs text-amber-600">{{ __('جارٍ رفع الملف...') }}</div>
@error('attachment') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
</div> </div>
{{-- Submit --}} {{-- Submit --}}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment