Merge pull request 'feat: Persian panel/API fixes and structural-model tweaks' (#1) from feat/persian-panel-fixes-and-model-tweaks into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -40,7 +40,7 @@ class AiProvidersTable
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -32,7 +32,7 @@ class CategoriesTable
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -37,7 +37,7 @@ class NotificationsTable
|
||||
->getStateUsing(fn ($record): bool => $record->read_at !== null),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -28,7 +28,7 @@ class PermissionsTable
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -28,7 +28,7 @@ class RolesTable
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -13,6 +13,10 @@ class TemplateGenerationInfolist
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextEntry::make('title')
|
||||
->label('نام فایل')
|
||||
->placeholder('—')
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('uuid')
|
||||
->label('شناسه')
|
||||
->copyable(),
|
||||
@@ -42,7 +46,7 @@ class TemplateGenerationInfolist
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime(),
|
||||
->jalaliDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,17 @@ class TemplateGenerationsTable
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('نام فایل')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->description(fn ($record): string => $record->uuid)
|
||||
->wrap(),
|
||||
TextColumn::make('uuid')
|
||||
->label('شناسه')
|
||||
->searchable()
|
||||
->copyable(),
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('template.title')
|
||||
->label('قالب')
|
||||
->searchable()
|
||||
@@ -44,7 +51,7 @@ class TemplateGenerationsTable
|
||||
->placeholder('—'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Filament\Resources\Templates\Pages;
|
||||
|
||||
use App\Filament\Resources\Templates\TemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
@@ -16,6 +18,8 @@ class EditTemplate extends EditRecord
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,15 @@ class GenerationsRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('uuid')
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('نام فایل')
|
||||
->searchable()
|
||||
->wrap(),
|
||||
TextColumn::make('uuid')
|
||||
->label('شناسه')
|
||||
->searchable()
|
||||
->copyable(),
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('status')
|
||||
->label('وضعیت')
|
||||
->badge()
|
||||
@@ -48,7 +53,7 @@ class GenerationsRelationManager extends RelationManager
|
||||
->placeholder('—'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
|
||||
@@ -46,7 +46,9 @@ class TemplateForm
|
||||
->label('تصویر بندانگشتی')
|
||||
->disk('public')
|
||||
->directory('templates/thumbnails')
|
||||
->image(),
|
||||
->visibility('public')
|
||||
->image()
|
||||
->imageEditor(),
|
||||
Toggle::make('is_active')
|
||||
->label('فعال')
|
||||
->default(true),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Templates\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class TemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
ImageEntry::make('thumbnail_path')
|
||||
->label('تصویر بندانگشتی')
|
||||
->disk('public')
|
||||
->visibility('public')
|
||||
->height(180)
|
||||
->placeholder('بدون تصویر')
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('title')
|
||||
->label('عنوان'),
|
||||
TextEntry::make('slug')
|
||||
->label('شناسه')
|
||||
->copyable(),
|
||||
TextEntry::make('category.name')
|
||||
->label('دستهبندی')
|
||||
->placeholder('—'),
|
||||
TextEntry::make('description')
|
||||
->label('توضیحات')
|
||||
->placeholder('—')
|
||||
->columnSpanFull(),
|
||||
IconEntry::make('is_active')
|
||||
->label('فعال')
|
||||
->boolean(),
|
||||
IconEntry::make('is_ai_enabled')
|
||||
->label('ساخت با هوش مصنوعی')
|
||||
->boolean(),
|
||||
TextEntry::make('ai_repeatable_slide_number')
|
||||
->label('شماره اسلاید تکرارشونده')
|
||||
->placeholder('—'),
|
||||
TextEntry::make('generations_count')
|
||||
->label('تعداد تولیدها')
|
||||
->state(fn ($record): int => $record->generations()->count()),
|
||||
TextEntry::make('creator.name')
|
||||
->label('ایجادکننده')
|
||||
->placeholder('—'),
|
||||
TextEntry::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->jalaliDateTime(),
|
||||
TextEntry::make('updated_at')
|
||||
->label('آخرین بروزرسانی')
|
||||
->jalaliDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,19 @@ namespace App\Filament\Resources\Templates\Tables;
|
||||
|
||||
use App\Filament\Resources\Templates\Actions\GenerateWithAiAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TemplatesTable
|
||||
@@ -35,7 +41,7 @@ class TemplatesTable
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
@@ -48,15 +54,22 @@ class TemplatesTable
|
||||
->placeholder('همه')
|
||||
->trueLabel('فعال')
|
||||
->falseLabel('غیرفعال'),
|
||||
TrashedFilter::make()
|
||||
->label('وضعیت حذف'),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
GenerateWithAiAction::make(),
|
||||
DeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Filament\Resources\Templates\Pages\ViewTemplate;
|
||||
use App\Filament\Resources\Templates\RelationManagers\GenerationsRelationManager;
|
||||
use App\Filament\Resources\Templates\RelationManagers\KeysRelationManager;
|
||||
use App\Filament\Resources\Templates\Schemas\TemplateForm;
|
||||
use App\Filament\Resources\Templates\Schemas\TemplateInfolist;
|
||||
use App\Filament\Resources\Templates\Tables\TemplatesTable;
|
||||
use App\Models\Template;
|
||||
use BackedEnum;
|
||||
@@ -16,6 +17,8 @@ use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use UnitEnum;
|
||||
|
||||
class TemplateResource extends Resource
|
||||
@@ -41,11 +44,22 @@ class TemplateResource extends Resource
|
||||
return TemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return TemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return TemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()
|
||||
->withoutGlobalScopes([SoftDeletingScope::class]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\Users\Schemas;
|
||||
|
||||
use App\Support\Mobile;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
@@ -25,6 +26,13 @@ class UserForm
|
||||
TextInput::make('mobile')
|
||||
->label('شماره موبایل')
|
||||
->tel()
|
||||
->helperText('مثال درست: 09123456789')
|
||||
->dehydrateStateUsing(fn (?string $state): ?string => Mobile::normalize($state))
|
||||
->rules([static function (string $attribute, mixed $value, \Closure $fail): void {
|
||||
if (filled($value) && ! preg_match(Mobile::REGEX, (string) Mobile::normalize((string) $value))) {
|
||||
$fail('فرمت شماره موبایل صحیح نیست. مثال درست: 09123456789');
|
||||
}
|
||||
}])
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(20),
|
||||
TextInput::make('password')
|
||||
|
||||
@@ -33,7 +33,7 @@ class UsersTable
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ ایجاد')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -46,7 +46,7 @@ class WalletTransactionsTable
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
|
||||
@@ -44,7 +44,7 @@ class TransactionsRelationManager extends RelationManager
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
|
||||
@@ -15,7 +15,7 @@ use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class WalletsTable
|
||||
{
|
||||
@@ -45,7 +45,7 @@ class WalletsTable
|
||||
->boolean(),
|
||||
TextColumn::make('updated_at')
|
||||
->label('آخرین بروزرسانی')
|
||||
->dateTime()
|
||||
->jalaliDateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
@@ -84,21 +84,27 @@ class WalletsTable
|
||||
->rows(2),
|
||||
])
|
||||
->action(function (Wallet $record, array $data, WalletService $walletService): void {
|
||||
$amount = (int) $data['amount'];
|
||||
$description = filled($data['description'] ?? null) ? $data['description'] : null;
|
||||
|
||||
try {
|
||||
$transaction = $type === 'credit'
|
||||
? $walletService->credit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user())
|
||||
: $walletService->debit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user());
|
||||
? $walletService->credit($record, $amount, $description, auth()->user())
|
||||
: $walletService->debit($record, $amount, $description, auth()->user());
|
||||
|
||||
Notification::make()
|
||||
->title($type === 'credit' ? 'واریز انجام شد' : 'برداشت انجام شد')
|
||||
->body('موجودی جدید: '.number_format($transaction->balance_after).' تومان')
|
||||
->success()
|
||||
->send();
|
||||
} catch (RuntimeException $exception) {
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('خطا')
|
||||
->body($exception->getMessage())
|
||||
->title($type === 'credit' ? 'واریز ناموفق بود' : 'برداشت ناموفق بود')
|
||||
->body($exception->getMessage() ?: 'خطای نامشخص هنگام ثبت تراکنش.')
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,6 +18,6 @@ class GenerationDownloadController extends Controller
|
||||
abort_unless($user && ($user->hasRole('admin') || $user->hasPermission('templates.manage')), 403);
|
||||
abort_if($generation->status !== TemplateGenerationStatus::Completed || ! $generation->output_path, 404);
|
||||
|
||||
return Storage::disk('local')->download($generation->output_path, $generation->template->slug.'.pptx');
|
||||
return Storage::disk('local')->download($generation->output_path, $generation->downloadFilename().'.pptx');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Profile\ChangePasswordRequest;
|
||||
use App\Http\Requests\Profile\DeleteAccountRequest;
|
||||
use App\Http\Requests\Profile\UpdateProfileRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Traits\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
@@ -158,10 +158,19 @@ class ProfileController extends Controller
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/profile',
|
||||
description: 'Delete the authenticated user account',
|
||||
description: 'Delete the authenticated user account. Requires the current password for confirmation.',
|
||||
summary: 'Delete account',
|
||||
security: [['sanctum' => []]],
|
||||
tags: ['Profile'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['password'],
|
||||
properties: [
|
||||
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -176,10 +185,14 @@ class ProfileController extends Controller
|
||||
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||
]
|
||||
)]
|
||||
public function destroy(Request $request): JsonResponse
|
||||
public function destroy(DeleteAccountRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->avatar_path) {
|
||||
Storage::disk('public')->delete($user->avatar_path);
|
||||
}
|
||||
|
||||
$user->tokens()->delete();
|
||||
$user->delete();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Http\Resources\TemplateGenerationResource;
|
||||
use App\Jobs\GenerateTemplateJob;
|
||||
use App\Models\Template;
|
||||
use App\Models\TemplateGeneration;
|
||||
use App\Support\Jalali;
|
||||
use App\Traits\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -80,6 +81,7 @@ class TemplateGenerationController extends Controller
|
||||
$generation = TemplateGeneration::query()->create([
|
||||
'template_id' => $template->id,
|
||||
'uuid' => $uuid,
|
||||
'title' => $this->buildGenerationTitle($template, $request),
|
||||
'status' => TemplateGenerationStatus::Pending,
|
||||
'values' => $storedValues,
|
||||
'requested_by' => $request->user()->id,
|
||||
@@ -264,6 +266,13 @@ class TemplateGenerationController extends Controller
|
||||
abort_if($generation->requested_by !== request()->user()->id, Response::HTTP_FORBIDDEN);
|
||||
abort_if($generation->status !== TemplateGenerationStatus::Completed || ! $generation->output_path, Response::HTTP_NOT_FOUND);
|
||||
|
||||
return Storage::disk('local')->download($generation->output_path, $generation->template->slug.'.pptx');
|
||||
return Storage::disk('local')->download($generation->output_path, $generation->downloadFilename().'.pptx');
|
||||
}
|
||||
|
||||
private function buildGenerationTitle(Template $template, Request $request): string
|
||||
{
|
||||
$label = $request->user()?->name ?: 'کاربر';
|
||||
|
||||
return sprintf('%s - %s - %s', $template->title, $label, Jalali::date(now()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,57 @@ use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateNotificationPreferencesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private const FIELDS = [
|
||||
'comment_on_presentation',
|
||||
'form_reply',
|
||||
'new_follower',
|
||||
'news_announcements',
|
||||
'weekly_product_updates',
|
||||
'weekly_blog_digest',
|
||||
];
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept "true"/"false"/"1"/"0"/1/0 strings from the frontend and coerce them to
|
||||
* real booleans before validation so the toggles actually persist.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach (self::FIELDS as $field) {
|
||||
if ($this->has($field)) {
|
||||
$normalized[$field] = filter_var($this->input($field), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($normalized !== []) {
|
||||
$this->merge($normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return array_fill_keys(self::FIELDS, ['sometimes', 'boolean']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'comment_on_presentation' => ['sometimes', 'boolean'],
|
||||
'form_reply' => ['sometimes', 'boolean'],
|
||||
'new_follower' => ['sometimes', 'boolean'],
|
||||
'news_announcements' => ['sometimes', 'boolean'],
|
||||
'weekly_product_updates' => ['sometimes', 'boolean'],
|
||||
'weekly_blog_digest' => ['sometimes', 'boolean'],
|
||||
'*.boolean' => 'مقدار این تنظیم باید بله یا خیر باشد.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use App\Support\Mobile;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
@@ -11,6 +12,13 @@ class RegisterRequest extends FormRequest
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('mobile')) {
|
||||
$this->merge(['mobile' => Mobile::normalize($this->input('mobile'))]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -18,7 +26,7 @@ class RegisterRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'mobile' => ['required', 'string', 'regex:/^09\d{9}$/', 'unique:users,mobile'],
|
||||
'mobile' => ['required', 'string', 'regex:'.Mobile::REGEX, 'unique:users,mobile'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'string', 'min:8'],
|
||||
];
|
||||
@@ -32,7 +40,7 @@ class RegisterRequest extends FormRequest
|
||||
return [
|
||||
'name.required' => 'نام و نام خانوادگی الزامی است.',
|
||||
'mobile.required' => 'شماره موبایل الزامی است.',
|
||||
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست.',
|
||||
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست. مثال درست: 09123456789',
|
||||
'mobile.unique' => 'این شماره موبایل قبلاً ثبت شده است.',
|
||||
'email.required' => 'ایمیل الزامی است.',
|
||||
'email.email' => 'فرمت ایمیل صحیح نیست.',
|
||||
|
||||
34
app/Http/Requests/Profile/DeleteAccountRequest.php
Normal file
34
app/Http/Requests/Profile/DeleteAccountRequest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Profile;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DeleteAccountRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'password' => ['required', 'string', 'current_password'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'password.required' => 'برای حذف حساب کاربری، وارد کردن رمز عبور الزامی است.',
|
||||
'password.current_password' => 'رمز عبور واردشده صحیح نیست.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\Profile;
|
||||
|
||||
use App\Support\Mobile;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -12,6 +13,13 @@ class UpdateProfileRequest extends FormRequest
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('mobile')) {
|
||||
$this->merge(['mobile' => Mobile::normalize($this->input('mobile'))]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -23,10 +31,16 @@ class UpdateProfileRequest extends FormRequest
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'username' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('users', 'username')->ignore($userId)],
|
||||
'email' => ['sometimes', 'required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)],
|
||||
'mobile' => ['sometimes', 'required', 'string', 'regex:/^09\d{9}$/', Rule::unique('users', 'mobile')->ignore($userId)],
|
||||
'mobile' => ['sometimes', 'required', 'string', 'regex:'.Mobile::REGEX, Rule::unique('users', 'mobile')->ignore($userId)],
|
||||
'password' => ['sometimes', 'required', 'string', 'min:8'],
|
||||
'avatar' => ['sometimes', 'nullable', 'image', 'max:3072'],
|
||||
'is_public' => ['sometimes', 'nullable', 'boolean'],
|
||||
'social_links' => ['sometimes', 'nullable', 'array'],
|
||||
'social_links.instagram' => ['nullable', 'string', 'max:255'],
|
||||
'social_links.telegram' => ['nullable', 'string', 'max:255'],
|
||||
'social_links.linkedin' => ['nullable', 'string', 'max:255'],
|
||||
'social_links.twitter' => ['nullable', 'string', 'max:255'],
|
||||
'social_links.website' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,12 +57,13 @@ class UpdateProfileRequest extends FormRequest
|
||||
'email.email' => 'فرمت ایمیل صحیح نیست.',
|
||||
'email.unique' => 'این ایمیل قبلاً ثبت شده است.',
|
||||
'mobile.required' => 'شماره موبایل الزامی است.',
|
||||
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست.',
|
||||
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست. مثال درست: 09123456789',
|
||||
'mobile.unique' => 'این شماره موبایل قبلاً ثبت شده است.',
|
||||
'password.required' => 'رمز عبور الزامی است.',
|
||||
'password.min' => 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
|
||||
'avatar.image' => 'فایل ارسالی باید تصویر باشد.',
|
||||
'avatar.max' => 'حداکثر حجم تصویر ۳ مگابایت است.',
|
||||
'social_links.array' => 'ساختار شبکههای اجتماعی نامعتبر است.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use OpenApi\Attributes as OA;
|
||||
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
|
||||
new OA\Property(property: 'avatar_url', type: 'string', example: 'https://example.com/storage/avatars/1.png', nullable: true),
|
||||
new OA\Property(property: 'is_public', type: 'boolean', example: false),
|
||||
new OA\Property(property: 'social_links', type: 'object', example: ['instagram' => 'https://instagram.com/handle'], nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
]
|
||||
)]
|
||||
@@ -37,6 +38,7 @@ class UserResource extends JsonResource
|
||||
'mobile' => $this->mobile,
|
||||
'avatar_url' => $this->avatar_path ? Storage::disk('public')->url($this->avatar_path) : null,
|
||||
'is_public' => $this->is_public,
|
||||
'social_links' => $this->social_links ?: null,
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable([
|
||||
'category_id',
|
||||
@@ -21,6 +22,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
])]
|
||||
class Template extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Category, $this>
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,9 @@ class TemplateGeneration extends Model
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Template::class);
|
||||
// Keep resolving the template even after it has been soft-deleted, so past
|
||||
// generations stay downloadable and keep their labels.
|
||||
return $this->belongsTo(Template::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +42,31 @@ class TemplateGeneration extends Model
|
||||
return $this->belongsTo(User::class, 'requested_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* A human-friendly name for this generation, used in the admin panel and as the
|
||||
* downloaded file name. Falls back to the template title plus a short id.
|
||||
*/
|
||||
public function displayName(): string
|
||||
{
|
||||
$base = $this->title ?: $this->template?->title ?: 'presentation';
|
||||
|
||||
return trim($base).' - '.$this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitized file name (without extension) for downloads. Keeps Persian characters
|
||||
* but drops anything a filesystem or HTTP header would choke on.
|
||||
*/
|
||||
public function downloadFilename(): string
|
||||
{
|
||||
$base = $this->title ?: $this->template?->title ?: $this->template?->slug ?: 'presentation';
|
||||
|
||||
$clean = preg_replace('/[\/\\\\?%*:|"<>\r\n]+/u', ' ', (string) $base) ?? '';
|
||||
$clean = trim(preg_replace('/\s+/u', ' ', $clean) ?? '');
|
||||
|
||||
return ($clean !== '' ? $clean : 'presentation').' - '.substr($this->uuid, 0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
#[Fillable(['name', 'username', 'email', 'mobile', 'password', 'avatar_path', 'is_public'])]
|
||||
#[Fillable(['name', 'username', 'email', 'mobile', 'password', 'avatar_path', 'is_public', 'social_links'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
@@ -104,6 +104,7 @@ class User extends Authenticatable implements FilamentUser
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_public' => 'boolean',
|
||||
'social_links' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ use App\Policies\UserPolicy;
|
||||
use App\Policies\WalletPolicy;
|
||||
use App\Policies\WalletTransactionPolicy;
|
||||
use App\Services\Payments\FakePaymentGateway;
|
||||
use App\Support\Jalali;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@@ -41,6 +44,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
$this->registerJalaliMacros();
|
||||
|
||||
User::observe(UserObserver::class);
|
||||
|
||||
Gate::policy(User::class, UserPolicy::class);
|
||||
@@ -50,4 +55,31 @@ class AppServiceProvider extends ServiceProvider
|
||||
Gate::policy(WalletTransaction::class, WalletTransactionPolicy::class);
|
||||
Gate::policy(Notification::class, NotificationPolicy::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ->jalaliDate() / ->jalaliDateTime() helpers to Filament table columns and
|
||||
* infolist entries so every date shown in the admin panel is Shamsi.
|
||||
*/
|
||||
private function registerJalaliMacros(): void
|
||||
{
|
||||
TextColumn::macro('jalaliDate', function () {
|
||||
/** @var TextColumn $this */
|
||||
return $this->formatStateUsing(fn ($state): ?string => Jalali::date($state));
|
||||
});
|
||||
|
||||
TextColumn::macro('jalaliDateTime', function () {
|
||||
/** @var TextColumn $this */
|
||||
return $this->formatStateUsing(fn ($state): ?string => Jalali::dateTime($state));
|
||||
});
|
||||
|
||||
TextEntry::macro('jalaliDate', function () {
|
||||
/** @var TextEntry $this */
|
||||
return $this->formatStateUsing(fn ($state): ?string => Jalali::date($state));
|
||||
});
|
||||
|
||||
TextEntry::macro('jalaliDateTime', function () {
|
||||
/** @var TextEntry $this */
|
||||
return $this->formatStateUsing(fn ($state): ?string => Jalali::dateTime($state));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
119
app/Support/Jalali.php
Normal file
119
app/Support/Jalali.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Lightweight Gregorian -> Jalali (Shamsi) converter, so the admin panel can show
|
||||
* Persian dates without pulling in an extra dependency. Only the conversion and a
|
||||
* couple of display formatters are implemented — enough for table columns and infolists.
|
||||
*/
|
||||
class Jalali
|
||||
{
|
||||
private const MONTHS = [
|
||||
1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور',
|
||||
7 => 'مهر', 8 => 'آبان', 9 => 'آذر', 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int, 2: int} [year, month, day]
|
||||
*/
|
||||
public static function fromGregorian(int $gy, int $gm, int $gd): array
|
||||
{
|
||||
$g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
|
||||
$gy2 = ($gm > 2) ? ($gy + 1) : $gy;
|
||||
$days = 355666 + (365 * $gy) + intdiv($gy2 + 3, 4) - intdiv($gy2 + 99, 100)
|
||||
+ intdiv($gy2 + 399, 400) + $gd + $g_d_m[$gm - 1];
|
||||
|
||||
$jy = -1595 + (33 * intdiv($days, 12053));
|
||||
$days %= 12053;
|
||||
|
||||
$jy += 4 * intdiv($days, 1461);
|
||||
$days %= 1461;
|
||||
|
||||
if ($days > 365) {
|
||||
$jy += intdiv($days - 1, 365);
|
||||
$days = ($days - 1) % 365;
|
||||
}
|
||||
|
||||
if ($days < 186) {
|
||||
$jm = 1 + intdiv($days, 31);
|
||||
$jd = 1 + ($days % 31);
|
||||
} else {
|
||||
$jm = 7 + intdiv($days - 186, 30);
|
||||
$jd = 1 + (($days - 186) % 30);
|
||||
}
|
||||
|
||||
return [$jy, $jm, $jd];
|
||||
}
|
||||
|
||||
public static function date(DateTimeInterface|CarbonInterface|string|null $value): ?string
|
||||
{
|
||||
$date = self::normalize($value);
|
||||
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$jy, $jm, $jd] = self::fromGregorian((int) $date->format('Y'), (int) $date->format('n'), (int) $date->format('j'));
|
||||
|
||||
return self::toPersianDigits(sprintf('%04d/%02d/%02d', $jy, $jm, $jd));
|
||||
}
|
||||
|
||||
public static function dateTime(DateTimeInterface|CarbonInterface|string|null $value): ?string
|
||||
{
|
||||
$date = self::normalize($value);
|
||||
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$jy, $jm, $jd] = self::fromGregorian((int) $date->format('Y'), (int) $date->format('n'), (int) $date->format('j'));
|
||||
|
||||
return self::toPersianDigits(sprintf(
|
||||
'%04d/%02d/%02d - %02d:%02d',
|
||||
$jy, $jm, $jd, (int) $date->format('H'), (int) $date->format('i')
|
||||
));
|
||||
}
|
||||
|
||||
public static function longDate(DateTimeInterface|CarbonInterface|string|null $value): ?string
|
||||
{
|
||||
$date = self::normalize($value);
|
||||
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$jy, $jm, $jd] = self::fromGregorian((int) $date->format('Y'), (int) $date->format('n'), (int) $date->format('j'));
|
||||
|
||||
return self::toPersianDigits($jd.' '.self::MONTHS[$jm].' '.$jy);
|
||||
}
|
||||
|
||||
private static function normalize(DateTimeInterface|CarbonInterface|string|null $value): ?DateTimeInterface
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
try {
|
||||
return new \DateTimeImmutable((string) $value);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static function toPersianDigits(string $value): string
|
||||
{
|
||||
return strtr($value, [
|
||||
'0' => '۰', '1' => '۱', '2' => '۲', '3' => '۳', '4' => '۴',
|
||||
'5' => '۵', '6' => '۶', '7' => '۷', '8' => '۸', '9' => '۹',
|
||||
]);
|
||||
}
|
||||
}
|
||||
41
app/Support/Mobile.php
Normal file
41
app/Support/Mobile.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Normalizes Iranian mobile numbers to the canonical "09XXXXXXXXX" form so that the
|
||||
* frontend can send numbers with Persian/Arabic digits, spaces, dashes or a country
|
||||
* code (+98 / 0098 / 98) without tripping the validation regex.
|
||||
*/
|
||||
class Mobile
|
||||
{
|
||||
public const REGEX = '/^09\d{9}$/';
|
||||
|
||||
public static function normalize(?string $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$digits = strtr(trim($value), [
|
||||
'۰' => '0', '۱' => '1', '۲' => '2', '۳' => '3', '۴' => '4',
|
||||
'۵' => '5', '۶' => '6', '۷' => '7', '۸' => '8', '۹' => '9',
|
||||
'٠' => '0', '١' => '1', '٢' => '2', '٣' => '3', '٤' => '4',
|
||||
'٥' => '5', '٦' => '6', '٧' => '7', '٨' => '8', '٩' => '9',
|
||||
]);
|
||||
|
||||
$digits = preg_replace('/[^\d+]/', '', $digits) ?? '';
|
||||
|
||||
if (str_starts_with($digits, '+98')) {
|
||||
$digits = '0'.substr($digits, 3);
|
||||
} elseif (str_starts_with($digits, '0098')) {
|
||||
$digits = '0'.substr($digits, 4);
|
||||
} elseif (str_starts_with($digits, '98') && strlen($digits) === 12) {
|
||||
$digits = '0'.substr($digits, 2);
|
||||
} elseif (strlen($digits) === 10 && str_starts_with($digits, '9')) {
|
||||
$digits = '0'.$digits;
|
||||
}
|
||||
|
||||
return $digits === '' ? null : $digits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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('templates', function (Blueprint $table): void {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('templates', function (Blueprint $table): void {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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('users', function (Blueprint $table): void {
|
||||
$table->json('social_links')->nullable()->after('is_public');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropColumn('social_links');
|
||||
});
|
||||
}
|
||||
};
|
||||
7
lang/fa/auth.php
Normal file
7
lang/fa/auth.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'failed' => 'اطلاعات ورود نادرست است.',
|
||||
'password' => 'رمز عبور واردشده صحیح نیست.',
|
||||
'throttle' => 'تلاشهای زیادی برای ورود انجام شده است. لطفاً بعد از :seconds ثانیه دوباره تلاش کنید.',
|
||||
];
|
||||
6
lang/fa/pagination.php
Normal file
6
lang/fa/pagination.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'previous' => '« قبلی',
|
||||
'next' => 'بعدی »',
|
||||
];
|
||||
9
lang/fa/passwords.php
Normal file
9
lang/fa/passwords.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'reset' => 'رمز عبور شما بازنشانی شد.',
|
||||
'sent' => 'لینک بازنشانی رمز عبور برای شما ایمیل شد.',
|
||||
'throttled' => 'لطفاً کمی صبر کنید و دوباره تلاش کنید.',
|
||||
'token' => 'این توکن بازنشانی رمز عبور نامعتبر است.',
|
||||
'user' => 'کاربری با این نشانی ایمیل یافت نشد.',
|
||||
];
|
||||
192
lang/fa/validation.php
Normal file
192
lang/fa/validation.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'accepted' => 'فیلد :attribute باید پذیرفته شود.',
|
||||
'accepted_if' => 'فیلد :attribute وقتی :other برابر :value است باید پذیرفته شود.',
|
||||
'active_url' => 'فیلد :attribute باید یک نشانی اینترنتی معتبر باشد.',
|
||||
'after' => 'فیلد :attribute باید تاریخی بعد از :date باشد.',
|
||||
'after_or_equal' => 'فیلد :attribute باید تاریخی بعد یا برابر با :date باشد.',
|
||||
'alpha' => 'فیلد :attribute باید فقط شامل حروف باشد.',
|
||||
'alpha_dash' => 'فیلد :attribute باید فقط شامل حروف، اعداد، خط تیره و زیرخط باشد.',
|
||||
'alpha_num' => 'فیلد :attribute باید فقط شامل حروف و اعداد باشد.',
|
||||
'any_of' => 'فیلد :attribute نامعتبر است.',
|
||||
'array' => 'فیلد :attribute باید آرایه باشد.',
|
||||
'ascii' => 'فیلد :attribute باید فقط شامل حروف و نمادهای تکبایتی باشد.',
|
||||
'before' => 'فیلد :attribute باید تاریخی قبل از :date باشد.',
|
||||
'before_or_equal' => 'فیلد :attribute باید تاریخی قبل یا برابر با :date باشد.',
|
||||
'between' => [
|
||||
'array' => 'فیلد :attribute باید بین :min تا :max آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید بین :min تا :max کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید بین :min تا :max باشد.',
|
||||
'string' => 'فیلد :attribute باید بین :min تا :max کاراکتر باشد.',
|
||||
],
|
||||
'boolean' => 'فیلد :attribute باید بله یا خیر باشد.',
|
||||
'can' => 'فیلد :attribute شامل مقدار غیرمجاز است.',
|
||||
'confirmed' => 'تکرار فیلد :attribute مطابقت ندارد.',
|
||||
'contains' => 'فیلد :attribute یک مقدار الزامی را ندارد.',
|
||||
'current_password' => 'رمز عبور صحیح نیست.',
|
||||
'date' => 'فیلد :attribute باید یک تاریخ معتبر باشد.',
|
||||
'date_equals' => 'فیلد :attribute باید تاریخی برابر با :date باشد.',
|
||||
'date_format' => 'فیلد :attribute باید با قالب :format مطابقت داشته باشد.',
|
||||
'decimal' => 'فیلد :attribute باید :decimal رقم اعشار داشته باشد.',
|
||||
'declined' => 'فیلد :attribute باید رد شود.',
|
||||
'declined_if' => 'فیلد :attribute وقتی :other برابر :value است باید رد شود.',
|
||||
'different' => 'فیلد :attribute و :other باید متفاوت باشند.',
|
||||
'digits' => 'فیلد :attribute باید :digits رقم باشد.',
|
||||
'digits_between' => 'فیلد :attribute باید بین :min تا :max رقم باشد.',
|
||||
'dimensions' => 'ابعاد تصویر فیلد :attribute نامعتبر است.',
|
||||
'distinct' => 'فیلد :attribute دارای مقدار تکراری است.',
|
||||
'doesnt_contain' => 'فیلد :attribute نباید شامل موارد زیر باشد: :values.',
|
||||
'doesnt_end_with' => 'فیلد :attribute نباید با یکی از موارد زیر تمام شود: :values.',
|
||||
'doesnt_start_with' => 'فیلد :attribute نباید با یکی از موارد زیر شروع شود: :values.',
|
||||
'email' => 'فیلد :attribute باید یک نشانی ایمیل معتبر باشد.',
|
||||
'encoding' => 'فیلد :attribute باید با :encoding کدگذاری شده باشد.',
|
||||
'ends_with' => 'فیلد :attribute باید با یکی از موارد زیر تمام شود: :values.',
|
||||
'enum' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'exists' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'extensions' => 'فیلد :attribute باید یکی از پسوندهای زیر را داشته باشد: :values.',
|
||||
'file' => 'فیلد :attribute باید یک فایل باشد.',
|
||||
'filled' => 'فیلد :attribute باید مقدار داشته باشد.',
|
||||
'gt' => [
|
||||
'array' => 'فیلد :attribute باید بیش از :value آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید بیشتر از :value کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید بزرگتر از :value باشد.',
|
||||
'string' => 'فیلد :attribute باید بیشتر از :value کاراکتر باشد.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'فیلد :attribute باید :value آیتم یا بیشتر داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید بزرگتر یا مساوی :value کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید بزرگتر یا مساوی :value باشد.',
|
||||
'string' => 'فیلد :attribute باید بزرگتر یا مساوی :value کاراکتر باشد.',
|
||||
],
|
||||
'hex_color' => 'فیلد :attribute باید یک رنگ هگزادسیمال معتبر باشد.',
|
||||
'image' => 'فیلد :attribute باید یک تصویر باشد.',
|
||||
'in' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'in_array' => 'فیلد :attribute باید در :other وجود داشته باشد.',
|
||||
'in_array_keys' => 'فیلد :attribute باید حداقل شامل یکی از کلیدهای زیر باشد: :values.',
|
||||
'integer' => 'فیلد :attribute باید عدد صحیح باشد.',
|
||||
'ip' => 'فیلد :attribute باید یک نشانی IP معتبر باشد.',
|
||||
'ipv4' => 'فیلد :attribute باید یک نشانی IPv4 معتبر باشد.',
|
||||
'ipv6' => 'فیلد :attribute باید یک نشانی IPv6 معتبر باشد.',
|
||||
'json' => 'فیلد :attribute باید یک رشته JSON معتبر باشد.',
|
||||
'list' => 'فیلد :attribute باید یک فهرست باشد.',
|
||||
'lowercase' => 'فیلد :attribute باید با حروف کوچک باشد.',
|
||||
'lt' => [
|
||||
'array' => 'فیلد :attribute باید کمتر از :value آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید کمتر از :value کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید کوچکتر از :value باشد.',
|
||||
'string' => 'فیلد :attribute باید کمتر از :value کاراکتر باشد.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => 'فیلد :attribute نباید بیش از :value آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید کوچکتر یا مساوی :value کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید کوچکتر یا مساوی :value باشد.',
|
||||
'string' => 'فیلد :attribute باید کوچکتر یا مساوی :value کاراکتر باشد.',
|
||||
],
|
||||
'mac_address' => 'فیلد :attribute باید یک نشانی MAC معتبر باشد.',
|
||||
'max' => [
|
||||
'array' => 'فیلد :attribute نباید بیش از :max آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute نباید بیشتر از :max کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute نباید بزرگتر از :max باشد.',
|
||||
'string' => 'فیلد :attribute نباید بیشتر از :max کاراکتر باشد.',
|
||||
],
|
||||
'max_digits' => 'فیلد :attribute نباید بیش از :max رقم داشته باشد.',
|
||||
'mimes' => 'فیلد :attribute باید فایلی از نوع: :values باشد.',
|
||||
'mimetypes' => 'فیلد :attribute باید فایلی از نوع: :values باشد.',
|
||||
'min' => [
|
||||
'array' => 'فیلد :attribute باید حداقل :min آیتم داشته باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید حداقل :min کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید حداقل :min باشد.',
|
||||
'string' => 'فیلد :attribute باید حداقل :min کاراکتر باشد.',
|
||||
],
|
||||
'min_digits' => 'فیلد :attribute باید حداقل :min رقم داشته باشد.',
|
||||
'missing' => 'فیلد :attribute نباید وجود داشته باشد.',
|
||||
'missing_if' => 'فیلد :attribute وقتی :other برابر :value است نباید وجود داشته باشد.',
|
||||
'missing_unless' => 'فیلد :attribute مگر اینکه :other برابر :value باشد نباید وجود داشته باشد.',
|
||||
'missing_with' => 'فیلد :attribute وقتی :values وجود دارد نباید وجود داشته باشد.',
|
||||
'missing_with_all' => 'فیلد :attribute وقتی :values وجود دارند نباید وجود داشته باشد.',
|
||||
'multiple_of' => 'فیلد :attribute باید مضربی از :value باشد.',
|
||||
'not_in' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'not_regex' => 'قالب فیلد :attribute نامعتبر است.',
|
||||
'numeric' => 'فیلد :attribute باید عدد باشد.',
|
||||
'password' => [
|
||||
'letters' => 'فیلد :attribute باید حداقل شامل یک حرف باشد.',
|
||||
'mixed' => 'فیلد :attribute باید حداقل شامل یک حرف بزرگ و یک حرف کوچک باشد.',
|
||||
'numbers' => 'فیلد :attribute باید حداقل شامل یک عدد باشد.',
|
||||
'symbols' => 'فیلد :attribute باید حداقل شامل یک نماد باشد.',
|
||||
'uncompromised' => 'فیلد :attribute در نشت اطلاعات دیده شده است. لطفاً مقدار دیگری انتخاب کنید.',
|
||||
],
|
||||
'present' => 'فیلد :attribute باید موجود باشد.',
|
||||
'present_if' => 'فیلد :attribute وقتی :other برابر :value است باید موجود باشد.',
|
||||
'present_unless' => 'فیلد :attribute مگر اینکه :other برابر :value باشد باید موجود باشد.',
|
||||
'present_with' => 'فیلد :attribute وقتی :values موجود است باید موجود باشد.',
|
||||
'present_with_all' => 'فیلد :attribute وقتی :values موجود هستند باید موجود باشد.',
|
||||
'prohibited' => 'فیلد :attribute ممنوع است.',
|
||||
'prohibited_if' => 'فیلد :attribute وقتی :other برابر :value است ممنوع است.',
|
||||
'prohibited_if_accepted' => 'فیلد :attribute وقتی :other پذیرفته شده است ممنوع است.',
|
||||
'prohibited_if_declined' => 'فیلد :attribute وقتی :other رد شده است ممنوع است.',
|
||||
'prohibited_unless' => 'فیلد :attribute مگر اینکه :other در :values باشد ممنوع است.',
|
||||
'prohibits' => 'فیلد :attribute از موجود بودن :other جلوگیری میکند.',
|
||||
'regex' => 'قالب فیلد :attribute نامعتبر است.',
|
||||
'required' => 'فیلد :attribute الزامی است.',
|
||||
'required_array_keys' => 'فیلد :attribute باید شامل مقادیری برای: :values باشد.',
|
||||
'required_if' => 'فیلد :attribute وقتی :other برابر :value است الزامی است.',
|
||||
'required_if_accepted' => 'فیلد :attribute وقتی :other پذیرفته شده است الزامی است.',
|
||||
'required_if_declined' => 'فیلد :attribute وقتی :other رد شده است الزامی است.',
|
||||
'required_unless' => 'فیلد :attribute مگر اینکه :other در :values باشد الزامی است.',
|
||||
'required_with' => 'فیلد :attribute وقتی :values موجود است الزامی است.',
|
||||
'required_with_all' => 'فیلد :attribute وقتی :values موجود هستند الزامی است.',
|
||||
'required_without' => 'فیلد :attribute وقتی :values موجود نیست الزامی است.',
|
||||
'required_without_all' => 'فیلد :attribute وقتی هیچکدام از :values موجود نیستند الزامی است.',
|
||||
'same' => 'فیلد :attribute باید با :other مطابقت داشته باشد.',
|
||||
'size' => [
|
||||
'array' => 'فیلد :attribute باید شامل :size آیتم باشد.',
|
||||
'file' => 'حجم فیلد :attribute باید :size کیلوبایت باشد.',
|
||||
'numeric' => 'فیلد :attribute باید برابر :size باشد.',
|
||||
'string' => 'فیلد :attribute باید :size کاراکتر باشد.',
|
||||
],
|
||||
'starts_with' => 'فیلد :attribute باید با یکی از موارد زیر شروع شود: :values.',
|
||||
'string' => 'فیلد :attribute باید رشته باشد.',
|
||||
'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
|
||||
'unique' => 'فیلد :attribute قبلاً استفاده شده است.',
|
||||
'uploaded' => 'بارگذاری فیلد :attribute ناموفق بود.',
|
||||
'uppercase' => 'فیلد :attribute باید با حروف بزرگ باشد.',
|
||||
'url' => 'فیلد :attribute باید یک نشانی اینترنتی معتبر باشد.',
|
||||
'ulid' => 'فیلد :attribute باید یک ULID معتبر باشد.',
|
||||
'uuid' => 'فیلد :attribute باید یک UUID معتبر باشد.',
|
||||
|
||||
'custom' => [
|
||||
'mobile' => [
|
||||
'regex' => 'فرمت شماره موبایل صحیح نیست. مثال درست: 09123456789',
|
||||
],
|
||||
],
|
||||
|
||||
'attributes' => [
|
||||
'name' => 'نام',
|
||||
'first_name' => 'نام',
|
||||
'last_name' => 'نام خانوادگی',
|
||||
'username' => 'نام کاربری',
|
||||
'email' => 'ایمیل',
|
||||
'mobile' => 'شماره موبایل',
|
||||
'phone' => 'شماره تماس',
|
||||
'password' => 'رمز عبور',
|
||||
'password_confirmation' => 'تکرار رمز عبور',
|
||||
'current_password' => 'رمز عبور فعلی',
|
||||
'title' => 'عنوان',
|
||||
'slug' => 'شناسه',
|
||||
'description' => 'توضیحات',
|
||||
'category_id' => 'دستهبندی',
|
||||
'amount' => 'مبلغ',
|
||||
'avatar' => 'تصویر پروفایل',
|
||||
'is_public' => 'نمایش عمومی پروفایل',
|
||||
'social_links' => 'شبکههای اجتماعی',
|
||||
'language' => 'زبان',
|
||||
'slide_count' => 'تعداد اسلایدها',
|
||||
'text' => 'متن',
|
||||
'roles' => 'نقشها',
|
||||
'file_path' => 'فایل پاورپوینت',
|
||||
'thumbnail_path' => 'تصویر بندانگشتی',
|
||||
],
|
||||
|
||||
];
|
||||
Reference in New Issue
Block a user