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;
}
}