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
'receipt_reference',
'expense_date',
'notes',
'attachment_path',
'attachment_name',
'status',
'cancelled_by',
'cancelled_at',
......
......@@ -9,12 +9,13 @@
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.app')]
#[Title('تسجيل مصروف')]
class ExpenseForm extends Component
{
use UsesBranchScope;
use UsesBranchScope, WithFileUploads;
public string $category = '';
public string $amount_display = '';
......@@ -24,6 +25,7 @@ class ExpenseForm extends Component
public string $receipt_reference = '';
public ?string $expense_date = null;
public string $notes = '';
public $attachment = null;
public function mount(): void
{
......@@ -42,6 +44,7 @@ public function rules(): array
'receipt_reference' => 'nullable|string|max:100',
'expense_date' => 'required|date',
'notes' => 'nullable|string',
'attachment' => 'nullable|file|max:5120|mimes:jpg,jpeg,png,pdf,webp',
];
}
......@@ -55,14 +58,29 @@ public function messages(): array
'description.max' => 'الوصف يجب ألا يتجاوز 500 حرف',
'payment_method.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
{
$this->validate();
try {
$attachmentPath = null;
$attachmentName = null;
if ($this->attachment) {
$attachmentName = $this->attachment->getClientOriginalName();
$attachmentPath = $this->attachment->store('expenses', 'public');
}
$service->recordExpense([
'branch_id' => $this->getActiveBranchId(),
'category' => $this->category,
......@@ -73,6 +91,8 @@ public function save(ExpenseService $service): void
'receipt_reference' => $this->receipt_reference ?: null,
'expense_date' => $this->expense_date,
'notes' => $this->notes ?: null,
'attachment_path' => $attachmentPath,
'attachment_name' => $attachmentName,
], auth()->user());
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">
<textarea wire:model="notes" rows="2"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"></textarea>
</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>
{{-- 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