187 lines
5.6 KiB
Markdown
187 lines
5.6 KiB
Markdown
# Libra - Coding Standards
|
|
|
|
> Extracted from `docs/architecture.md` Section 20
|
|
|
|
## Critical Rules
|
|
|
|
| Rule | Description |
|
|
|------|-------------|
|
|
| **Volt Pattern** | Use class-based Volt components (per CLAUDE.md) |
|
|
| **Flux UI** | Use Flux components when available |
|
|
| **Form Validation** | Always use Form Request or Livewire form objects |
|
|
| **Eloquent** | Prefer `Model::query()` over `DB::` facade |
|
|
| **Actions** | Use single-purpose Action classes for business logic |
|
|
| **Testing** | Every feature must have corresponding Pest tests |
|
|
| **Pint** | Run `vendor/bin/pint --dirty` before committing |
|
|
|
|
## Naming Conventions
|
|
|
|
| Element | Convention | Example |
|
|
|---------|------------|---------|
|
|
| Models | Singular PascalCase | `Consultation` |
|
|
| Tables | Plural snake_case | `consultations` |
|
|
| Columns | snake_case | `booking_date` |
|
|
| Controllers | PascalCase + Controller | `ConsultationController` |
|
|
| Actions | Verb + Noun + Action | `CreateConsultationAction` |
|
|
| Jobs | Verb + Noun | `SendBookingNotification` |
|
|
| Events | Past tense | `ConsultationApproved` |
|
|
| Listeners | Verb phrase | `SendApprovalNotification` |
|
|
| Volt Components | kebab-case path | `admin/consultations/index` |
|
|
| Enums | PascalCase | `ConsultationStatus` |
|
|
| Enum Cases | PascalCase | `NoShow` |
|
|
| Traits | Adjective or -able | `HasConsultations`, `Bookable` |
|
|
| Interfaces | Adjective or -able | `Exportable` |
|
|
|
|
## Volt Component Template
|
|
|
|
```php
|
|
<?php
|
|
// resources/views/livewire/admin/consultations/index.blade.php
|
|
|
|
use App\Models\Consultation;
|
|
use App\Enums\ConsultationStatus;
|
|
use Livewire\Volt\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
new class extends Component {
|
|
use WithPagination;
|
|
|
|
public string $search = '';
|
|
public string $status = '';
|
|
|
|
public function updatedSearch(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedStatus(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'consultations' => Consultation::query()
|
|
->with('user:id,full_name,email')
|
|
->when($this->search, fn ($q) => $q->whereHas('user', fn ($q) =>
|
|
$q->where('full_name', 'like', "%{$this->search}%")
|
|
))
|
|
->when($this->status, fn ($q) => $q->where('status', $this->status))
|
|
->orderBy('booking_date', 'desc')
|
|
->paginate(15),
|
|
'statuses' => ConsultationStatus::cases(),
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<x-slot name="header">
|
|
<flux:heading size="xl">{{ __('models.consultations') }}</flux:heading>
|
|
</x-slot>
|
|
|
|
<div class="flex gap-4 mb-6">
|
|
<flux:input
|
|
wire:model.live.debounce.300ms="search"
|
|
placeholder="{{ __('messages.search') }}"
|
|
icon="magnifying-glass"
|
|
/>
|
|
|
|
<flux:select wire:model.live="status">
|
|
<option value="">{{ __('messages.all_statuses') }}</option>
|
|
@foreach($statuses as $s)
|
|
<option value="{{ $s->value }}">{{ $s->label() }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
@forelse($consultations as $consultation)
|
|
<x-ui.card wire:key="consultation-{{ $consultation->id }}">
|
|
{{-- Card content --}}
|
|
</x-ui.card>
|
|
@empty
|
|
<x-ui.empty-state :message="__('messages.no_consultations')" />
|
|
@endforelse
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
{{ $consultations->links() }}
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## Action Class Template
|
|
|
|
```php
|
|
<?php
|
|
// app/Actions/Consultation/CreateConsultationAction.php
|
|
|
|
namespace App\Actions\Consultation;
|
|
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\PaymentStatus;
|
|
use App\Jobs\SendBookingNotification;
|
|
use App\Exceptions\BookingLimitExceededException;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class CreateConsultationAction
|
|
{
|
|
public function execute(User $user, array $data): Consultation
|
|
{
|
|
// Validate booking limit
|
|
if ($user->hasBookingOnDate($data['booking_date'])) {
|
|
throw new BookingLimitExceededException(
|
|
__('messages.booking_limit_exceeded')
|
|
);
|
|
}
|
|
|
|
return DB::transaction(function () use ($user, $data) {
|
|
$consultation = Consultation::create([
|
|
'user_id' => $user->id,
|
|
'booking_date' => $data['booking_date'],
|
|
'booking_time' => $data['booking_time'],
|
|
'problem_summary' => $data['problem_summary'],
|
|
'status' => ConsultationStatus::Pending,
|
|
'payment_status' => PaymentStatus::NotApplicable,
|
|
]);
|
|
|
|
SendBookingNotification::dispatch($consultation);
|
|
|
|
return $consultation;
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Standards
|
|
|
|
- Use **Pest 4** with `Volt::test()` for Livewire components
|
|
- Every feature must have corresponding tests
|
|
- Use factories with custom states when creating test models
|
|
- Follow existing test structure in `tests/Feature/` and `tests/Unit/`
|
|
|
|
### Test Example
|
|
|
|
```php
|
|
use Livewire\Volt\Volt;
|
|
|
|
test('consultation list can be filtered by status', function () {
|
|
$user = User::factory()->admin()->create();
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->actingAs($user)
|
|
->set('status', ConsultationStatus::Pending->value)
|
|
->assertHasNoErrors();
|
|
});
|
|
```
|
|
|
|
## Bilingual Support
|
|
|
|
- All user-facing strings must use Laravel's `__()` helper
|
|
- Translations stored in `resources/lang/{ar,en}/`
|
|
- Use JSON translations for simple strings, PHP arrays for structured data
|
|
- RTL layout handled via Tailwind CSS classes
|