getRecord() !== null; return $form ->schema([ Section::make('اطلاعات فاکتور') ->schema([ Select::make('user_id') ->label('مشتری') ->options(fn () => User::role('customer')->pluck('name', 'id')) ->searchable() ->required() ->disabled($isEdit) ->helperText($isEdit ? 'مشتری بعد از صدور فاکتور قابل تغییر نیست.' : 'کاربری که فاکتور برایش صادر می‌شود.'), TextInput::make('invoice_number') ->label('شماره فاکتور') ->disabled() ->dehydrated(false) ->hidden(fn ($record) => $record === null) ->formatStateUsing(fn ($record) => $record?->invoice_number ?? '— تولید خودکار بعد از ذخیره —'), DatePicker::make('due_date') ->label('موعد پرداخت (اختیاری)') ->minDate(now()->toDateString()) ->helperText('بعد از این تاریخ، فاکتور به‌صورت خودکار منقضی می‌شود.'), ])->columns(2), Section::make('اقلام فاکتور') ->description('اقلامی که به مشتری فاکتور می‌شوند. هر قلم: شرح، تعداد، قیمت واحد (ریال). اعداد به‌صورت خودکار ۳ رقم جدا می‌شوند.') ->schema([ Repeater::make('items') ->label('اقلام') ->schema([ TextInput::make('description') ->label('شرح خدمت/کالا') ->required() ->columnSpan(2), TextInput::make('quantity') ->label('تعداد') ->numeric() ->default(1) ->minValue(0.01) ->step(0.01) ->required(), TextInput::make('unit_price') ->label('قیمت واحد (ریال)') ->numeric() ->minValue(0) ->step(1) ->required() ->suffix('ریال') ->formatStateUsing(fn ($state) => is_numeric($state) && $state > 0 ? number_format((float) $state, 0, '.', ',') : $state) ->dehydrateStateUsing(fn ($state) => is_string($state) ? (float) str_replace(',', '', $state) : (float) $state) ->live() ->afterStateUpdated(function (Get $get, Forms\Set $set) { self::recalcTotals($get, $set); }), ]) ->columns(4) ->collapsible() ->itemLabel(fn (array $state) => ($state['description'] ?? 'قلم جدید')) ->defaultItems(1) ->live() ->afterStateUpdated(function (Get $get, Forms\Set $set) { self::recalcTotals($get, $set); }) ->deleteAction(fn ($action) => $action->after(fn (Get $get, Forms\Set $set) => self::recalcTotals($get, $set))) ->reorderable(false), ]), Section::make('تخفیف و مبالغ نهایی') ->schema([ TextInput::make('discount_amount') ->label('مبلغ تخفیف (ریال)') ->numeric() ->default(0) ->minValue(0) ->step(1) ->suffix('ریال') ->formatStateUsing(fn ($state) => is_numeric($state) && $state > 0 ? number_format((float) $state, 0, '.', ',') : $state) ->dehydrateStateUsing(fn ($state) => is_string($state) ? (float) str_replace(',', '', $state) : (float) $state) ->live() ->afterStateUpdated(fn (Get $get, Forms\Set $set) => self::recalcTotals($get, $set)), Placeholder::make('subtotal_display') ->label('مجموع اقلام (قبل از تخفیف)') ->content(function (Get $get): HtmlString { $items = $get('items') ?? []; $subtotal = 0; foreach ($items as $item) { $rawQty = $item['quantity'] ?? 0; $rawUnit = $item['unit_price'] ?? 0; // پشتیبانی از اعداد با کاما (مثلاً "1,000,000") $qty = is_string($rawQty) ? (float) str_replace(',', '', $rawQty) : (float) $rawQty; $unit = is_string($rawUnit) ? (float) str_replace(',', '', $rawUnit) : (float) $rawUnit; $subtotal += $qty * $unit; } return new HtmlString('' . number_format($subtotal) . ' ریال'); }) ->live(), Placeholder::make('total_amount_display') ->label('مبلغ نهایی قابل پرداخت') ->content(function (Get $get): HtmlString { $items = $get('items') ?? []; $subtotal = 0; foreach ($items as $item) { $rawQty = $item['quantity'] ?? 0; $rawUnit = $item['unit_price'] ?? 0; // پشتیبانی از اعداد با کاما $qty = is_string($rawQty) ? (float) str_replace(',', '', $rawQty) : (float) $rawQty; $unit = is_string($rawUnit) ? (float) str_replace(',', '', $rawUnit) : (float) $rawUnit; $subtotal += $qty * $unit; } $rawDiscount = $get('discount_amount') ?? 0; $discount = is_string($rawDiscount) ? (float) str_replace(',', '', $rawDiscount) : (float) $rawDiscount; $total = max(0, $subtotal - $discount); return new HtmlString( '' . number_format($total) . ' ریال' ); }) ->live(), ])->columns(2), Section::make('توضیحات') ->schema([ Textarea::make('notes_to_customer') ->label('توضیحات برای مشتری') ->rows(3) ->helperText('این متن در پورتال مشتری نمایش داده می‌شود.'), Textarea::make('admin_notes') ->label('یادداشت داخلی (نمایش داده نمی‌شود به مشتری)') ->rows(2), ])->columns(2), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('invoice_number') ->label('شماره فاکتور') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('user.name') ->label('مشتری') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('total_amount') ->label('مبلغ نهایی') ->formatStateUsing(fn ($state) => number_format($state) . ' ریال') ->sortable(), Tables\Columns\TextColumn::make('items_count') ->label('تعداد اقلام') ->state(fn (ManualInvoice $record) => count($record->items ?? [])), Tables\Columns\TextColumn::make('status') ->label('وضعیت') ->badge() ->formatStateUsing(fn ($state) => match($state) { 'pending_payment' => 'در انتظار پرداخت', 'paid' => 'پرداخت شده', 'cancelled' => 'لغو شده', 'expired' => 'منقضی', default => $state, }) ->color(fn ($state) => match($state) { 'pending_payment' => 'warning', 'paid' => 'success', 'cancelled' => 'danger', 'expired' => 'gray', default => 'gray', }), Tables\Columns\TextColumn::make('due_date') ->label('موعد پرداخت') ->date('Y-m-d') ->placeholder('—'), Tables\Columns\TextColumn::make('paid_at') ->label('تاریخ پرداخت') ->dateTime('Y-m-d H:i') ->placeholder('—'), Tables\Columns\TextColumn::make('issuedBy.name') ->label('صادرکننده') ->placeholder('—'), Tables\Columns\TextColumn::make('created_at') ->label('تاریخ صدور') ->dateTime('Y-m-d H:i') ->sortable(), ]) ->filters([ Tables\Filters\SelectFilter::make('status') ->label('وضعیت') ->options([ 'pending_payment' => 'در انتظار پرداخت', 'paid' => 'پرداخت شده', 'cancelled' => 'لغو شده', 'expired' => 'منقضی', ]), ]) ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make() ->visible(fn (ManualInvoice $record) => $record->isPending()), Action::make('cancel') ->label('لغو فاکتور') ->icon('heroicon-o-x-circle') ->color('danger') ->visible(fn (ManualInvoice $record) => $record->isPending()) ->requiresConfirmation() ->form([ Textarea::make('reason') ->label('علت لغو (اختیاری)') ->rows(2), ]) ->action(function (ManualInvoice $record, array $data) { try { $service = app(ManualInvoiceService::class); $service->cancelInvoice($record, auth()->id(), $data['reason'] ?? null); Notification::make() ->title('فاکتور لغو شد.') ->success() ->send(); } catch (\Throwable $e) { Notification::make() ->title('خطا در لغو فاکتور') ->body($e->getMessage()) ->danger() ->send(); } }), ]) ->bulkActions([]) ->defaultSort('created_at', 'desc'); } public static function getPages(): array { return [ 'index' => \App\Filament\Resources\ManualInvoiceResource\Pages\ListManualInvoices::route('/'), 'create' => \App\Filament\Resources\ManualInvoiceResource\Pages\CreateManualInvoice::route('/create'), 'view' => \App\Filament\Resources\ManualInvoiceResource\Pages\ViewManualInvoice::route('/{record}'), 'edit' => \App\Filament\Resources\ManualInvoiceResource\Pages\EditManualInvoice::route('/{record}/edit'), ]; } /** * محاسبه و تنظیم subtotal و total در فرم. */ public static function recalcTotals(Get $get, Forms\Set $set): void { $items = $get('items') ?? []; $subtotal = 0; foreach ($items as $item) { $qty = (float) ($item['quantity'] ?? 0); $unit = (float) ($item['unit_price'] ?? 0); $subtotal += $qty * $unit; } $discount = (float) ($get('discount_amount') ?? 0); $total = max(0, $subtotal - $discount); } /** * بعد از ایجاد رکورد، شماره فاکتور رو تولید کن و ذخیره کن. */ public static function mutateFormDataBeforeCreate(array $data): array { // محاسبه subtotal و total $totals = ManualInvoice::calculateTotal($data['items'] ?? [], (float) ($data['discount_amount'] ?? 0)); $data['subtotal'] = $totals['subtotal']; $data['discount_amount'] = $totals['discount_amount']; $data['total_amount'] = $totals['total_amount']; $data['issued_by'] = auth()->id(); $data['invoice_number'] = ManualInvoice::generateInvoiceNumber(); $data['status'] = 'pending_payment'; return $data; } }