Compare commits

...

8 Commits

81 changed files with 2940 additions and 392 deletions

View File

@@ -6,7 +6,7 @@ APP_URL=http://localhost
SMS_API_URL=https://sms.daragt.com/api/sms SMS_API_URL=https://sms.daragt.com/api/sms
APP_LOCALE=en APP_LOCALE=tk
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US

View File

@@ -1,58 +1 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> # App
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Pages; namespace App\Filament\Pages;
use App\Models\Coupon; use App\Models\Coupon;
use App\Models\SmsCampaign;
use App\Services\SmsBroadcastService; use App\Services\SmsBroadcastService;
use App\Services\SmsMessageAnalyzer; use App\Services\SmsMessageAnalyzer;
use BackedEnum; use BackedEnum;
@@ -30,9 +29,9 @@ class SendSms extends Page
{ {
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChatBubbleLeftRight; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChatBubbleLeftRight;
protected static ?string $navigationLabel = 'Send SMS'; protected static ?string $navigationLabel = null;
protected static ?string $title = 'Send SMS'; protected static ?string $title = null;
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
@@ -43,9 +42,15 @@ class SendSms extends Page
*/ */
public ?array $data = []; public ?array $data = [];
public ?int $activeCampaignId = null; public static function getNavigationLabel(): string
{
return __('filament.send_sms.navigation');
}
public bool $campaignNotificationSent = false; public function getTitle(): string
{
return __('filament.send_sms.title');
}
public function mount(): void public function mount(): void
{ {
@@ -68,38 +73,37 @@ class SendSms extends Page
{ {
return $schema return $schema
->components([ ->components([
Section::make('Message') Section::make(__('filament.send_sms.sections.message'))
->description('Write the SMS exactly as recipients should see it.')
->schema([ ->schema([
Textarea::make('message') Textarea::make('message')
->label('SMS message') ->label(__('filament.send_sms.fields.message'))
->required() ->required()
->rows(8) ->rows(8)
->maxLength(1000) ->maxLength(1000)
->live(debounce: 300) ->live(debounce: 300)
->helperText(fn (Get $get): string => app(SmsMessageAnalyzer::class)->summary((string) ($get('message') ?? ''))), ->helperText(fn (Get $get): string => app(SmsMessageAnalyzer::class)->summary((string) ($get('message') ?? ''))),
]), ]),
Section::make('Recipients') Section::make(__('filament.send_sms.sections.recipients'))
->description('Send to everyone, pick specific coupon holders, or exclude numbers from a broadcast.') ->description(__('filament.send_sms.sections.recipients_description'))
->schema([ ->schema([
Checkbox::make('send_to_all') Checkbox::make('send_to_all')
->label('Send to all coupon holders') ->label(__('filament.send_sms.fields.send_to_all'))
->helperText('When enabled, every coupon holder is included. Manual selection is disabled.') ->helperText(__('filament.send_sms.fields.send_to_all_helper'))
->live(), ->live(),
Select::make('coupon_ids') Select::make('coupon_ids')
->label('Select recipients') ->label(__('filament.send_sms.fields.coupon_ids'))
->multiple() ->multiple()
->searchable() ->searchable()
->disabled(fn (Get $get): bool => (bool) $get('send_to_all')) ->disabled(fn (Get $get): bool => (bool) $get('send_to_all'))
->dehydrated(fn (Get $get): bool => ! (bool) $get('send_to_all')) ->dehydrated(fn (Get $get): bool => ! (bool) $get('send_to_all'))
->helperText('Search by phone, coupon code, or ID.') ->helperText(__('filament.send_sms.fields.coupon_ids_helper'))
->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search)) ->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search))
->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)), ->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)),
Select::make('excluded_coupon_ids') Select::make('excluded_coupon_ids')
->label('Exclude recipients') ->label(__('filament.send_sms.fields.excluded_coupon_ids'))
->multiple() ->multiple()
->searchable() ->searchable()
->helperText('Excluded numbers will not receive this message, even when sending to all.') ->helperText(__('filament.send_sms.fields.excluded_coupon_ids_helper'))
->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search)) ->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search))
->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)), ->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)),
]), ]),
@@ -116,14 +120,13 @@ class SendSms extends Page
->footer([ ->footer([
Actions::make([ Actions::make([
Action::make('send') Action::make('send')
->label('Send SMS') ->label(__('filament.send_sms.actions.send'))
->icon(Heroicon::OutlinedPaperAirplane) ->icon(Heroicon::OutlinedPaperAirplane)
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Send SMS now?') ->modalHeading(__('filament.send_sms.actions.send_confirm_heading'))
->modalDescription(fn (): string => $this->confirmationDescription()) ->modalDescription(fn (): string => $this->confirmationDescription())
->modalSubmitActionLabel('Send') ->modalSubmitActionLabel(__('filament.send_sms.actions.send_confirm_submit'))
->action('sendCampaign') ->action('sendCampaign'),
->disabled(fn (): bool => $this->activeCampaignId !== null && ! $this->isCampaignComplete()),
]), ]),
]), ]),
]); ]);
@@ -131,15 +134,6 @@ class SendSms extends Page
public function sendCampaign(): void public function sendCampaign(): void
{ {
if ($this->activeCampaignId !== null && ! $this->isCampaignComplete()) {
Notification::make()
->title('A campaign is still sending')
->warning()
->send();
return;
}
$data = $this->form->getState(); $data = $this->form->getState();
$validator = Validator::make($data, [ $validator = Validator::make($data, [
@@ -161,110 +155,53 @@ class SendSms extends Page
$excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []); $excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []);
$service = app(SmsBroadcastService::class); $service = app(SmsBroadcastService::class);
$willReceive = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds) $recipients = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds)
->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone)) ->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone));
->count();
$willReceive = $recipients->count();
if ($willReceive === 0) { if ($willReceive === 0) {
Notification::make() Notification::make()
->title('No valid recipients') ->title(__('filament.send_sms.notifications.no_valid_recipients'))
->body('Adjust your selection or exclusions — no coupon holders with valid phone numbers will receive this message.') ->body(__('filament.send_sms.notifications.no_valid_recipients_body'))
->danger() ->danger()
->send(); ->send();
return; return;
} }
$campaign = $service->startCampaign( $messages = $recipients->map(fn (Coupon $coupon): array => [
admin: auth()->user(), 'phone' => '+993'.$coupon->phone,
message: $data['message'], 'message' => $data['message'],
sendToAll: $sendToAll, ])->values()->all();
selectedIds: $selectedIds,
excludedIds: $excludedIds,
);
$this->activeCampaignId = $campaign->id; $success = sendBulkSMS($messages);
$this->campaignNotificationSent = false;
if ($campaign->isComplete()) { if ($success) {
$this->notifyCampaignFinished($campaign);
} else {
Notification::make() Notification::make()
->title('SMS campaign queued') ->title(__('filament.send_sms.notifications.submitted'))
->body("Sending to {$campaign->recipient_count} recipient(s) in the background.") ->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
->success() ->success()
->send(); ->send();
} } else {
}
public function refreshCampaignProgress(): void
{
if ($this->activeCampaignId === null) {
return;
}
$campaign = SmsCampaign::query()->find($this->activeCampaignId);
if ($campaign === null) {
$this->activeCampaignId = null;
return;
}
if ($campaign->isComplete()) {
$this->notifyCampaignFinished($campaign);
}
}
protected function notifyCampaignFinished(SmsCampaign $campaign): void
{
if ($this->campaignNotificationSent) {
return;
}
$this->campaignNotificationSent = true;
$campaign->refresh();
if ($campaign->failed_count > 0 && $campaign->sent_count === 0) {
Notification::make() Notification::make()
->title('SMS campaign failed') ->title(__('filament.send_sms.notifications.failed'))
->body("{$campaign->failed_count} message(s) failed to send.") ->body(__('filament.send_sms.notifications.failed_body'))
->danger() ->danger()
->send(); ->send();
} elseif ($campaign->failed_count > 0) {
Notification::make()
->title('SMS campaign finished with errors')
->body("Sent {$campaign->sent_count}, failed {$campaign->failed_count}, skipped {$campaign->skipped_count}.")
->warning()
->send();
} else {
Notification::make()
->title('SMS campaign completed')
->body("Successfully sent {$campaign->sent_count} message(s).")
->success()
->send();
} }
} }
protected function isCampaignComplete(): bool
{
if ($this->activeCampaignId === null) {
return true;
}
return SmsCampaign::query()
->whereKey($this->activeCampaignId)
->whereNotNull('completed_at')
->exists();
}
protected function confirmationDescription(): string protected function confirmationDescription(): string
{ {
$message = (string) ($this->data['message'] ?? ''); $message = (string) ($this->data['message'] ?? '');
$analyzer = app(SmsMessageAnalyzer::class); $analyzer = app(SmsMessageAnalyzer::class);
$willReceive = $this->getWillReceiveCount(); $willReceive = $this->getWillReceiveCount();
return "Send to {$willReceive} recipient(s)? {$analyzer->summary($message)}"; return __('filament.send_sms.confirmation', [
'count' => $willReceive,
'summary' => $analyzer->summary($message),
]);
} }
public function getWillReceiveCount(): int public function getWillReceiveCount(): int
@@ -281,43 +218,6 @@ class SendSms extends Page
->count(); ->count();
} }
public function getCampaignProgressPercent(): int
{
if ($this->activeCampaignId === null) {
return 0;
}
$campaign = SmsCampaign::query()->find($this->activeCampaignId);
return $campaign?->progressPercent() ?? 0;
}
public function getCampaignProgressLabel(): string
{
if ($this->activeCampaignId === null) {
return '';
}
$campaign = SmsCampaign::query()->find($this->activeCampaignId);
if ($campaign === null) {
return '';
}
return "{$campaign->processedCount()} / {$campaign->recipient_count} processed · {$campaign->sent_count} sent";
}
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
return [
'campaignProgressPercent' => $this->getCampaignProgressPercent(),
'campaignProgressLabel' => $this->getCampaignProgressLabel(),
];
}
/** /**
* @return array<int, string> * @return array<int, string>
*/ */
@@ -368,6 +268,6 @@ class SendSms extends Page
protected function couponLabel(Coupon $coupon): string protected function couponLabel(Coupon $coupon): string
{ {
return format_phone($coupon->phone).' · #'.$coupon->id.' · '.$coupon->code; return format_phone($coupon->phone);
} }
} }

View File

@@ -19,12 +19,17 @@ class CouponResource extends Resource
protected static ?string $recordTitleAttribute = 'phone'; protected static ?string $recordTitleAttribute = 'phone';
protected static ?string $navigationLabel = 'Coupons'; protected static ?string $navigationLabel = null;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTicket; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTicket;
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 1;
public static function getNavigationLabel(): string
{
return __('filament.coupons.navigation');
}
public static function infolist(Schema $schema): Schema public static function infolist(Schema $schema): Schema
{ {
return CouponInfolist::configure($schema); return CouponInfolist::configure($schema);

View File

@@ -12,13 +12,17 @@ class CouponInfolist
return $schema return $schema
->components([ ->components([
TextEntry::make('phone') TextEntry::make('phone')
->label(__('filament.fields.phone'))
->formatStateUsing(fn (string $state): string => format_phone($state)) ->formatStateUsing(fn (string $state): string => format_phone($state))
->copyable(), ->copyable(),
TextEntry::make('code') TextEntry::make('code')
->label(__('filament.fields.code'))
->copyable(), ->copyable(),
TextEntry::make('created_at') TextEntry::make('created_at')
->label(__('filament.fields.created_at'))
->dateTime(), ->dateTime(),
TextEntry::make('updated_at') TextEntry::make('updated_at')
->label(__('filament.fields.updated_at'))
->dateTime(), ->dateTime(),
]); ]);
} }

View File

@@ -3,8 +3,8 @@
namespace App\Filament\Resources\Coupons\Tables; namespace App\Filament\Resources\Coupons\Tables;
use App\Filament\Tables\Filters\CreatedAtDateFilter; use App\Filament\Tables\Filters\CreatedAtDateFilter;
use Filament\Actions\ViewAction;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -16,19 +16,24 @@ class CouponsTable
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->columns([ ->columns([
TextColumn::make('id') TextColumn::make('id')
->label(__('filament.fields.id'))
->sortable(), ->sortable(),
TextColumn::make('phone') TextColumn::make('phone')
->label(__('filament.fields.phone'))
->formatStateUsing(fn (string $state): string => format_phone($state)) ->formatStateUsing(fn (string $state): string => format_phone($state))
->searchable() ->searchable()
->sortable() ->sortable()
->copyable(), ->copyable(),
TextColumn::make('code') TextColumn::make('code')
->label(__('filament.fields.code'))
->sortable() ->sortable()
->copyable(), ->copyable(),
TextColumn::make('created_at') TextColumn::make('created_at')
->label(__('filament.fields.created_at'))
->dateTime() ->dateTime()
->sortable(), ->sortable(),
TextColumn::make('updated_at') TextColumn::make('updated_at')
->label(__('filament.fields.updated_at'))
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),

View File

@@ -19,16 +19,31 @@ class PhoneVerificationResource extends Resource
protected static ?string $recordTitleAttribute = 'phone'; protected static ?string $recordTitleAttribute = 'phone';
protected static ?string $navigationLabel = 'OTP Verifications'; protected static ?string $navigationLabel = null;
protected static ?string $modelLabel = 'OTP verification'; protected static ?string $modelLabel = null;
protected static ?string $pluralModelLabel = 'OTP verifications'; protected static ?string $pluralModelLabel = null;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedKey; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedKey;
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
public static function getNavigationLabel(): string
{
return __('filament.phone_verifications.navigation');
}
public static function getModelLabel(): string
{
return __('filament.phone_verifications.model');
}
public static function getPluralModelLabel(): string
{
return __('filament.phone_verifications.plural');
}
public static function infolist(Schema $schema): Schema public static function infolist(Schema $schema): Schema
{ {
return PhoneVerificationInfolist::configure($schema); return PhoneVerificationInfolist::configure($schema);

View File

@@ -13,15 +13,20 @@ class PhoneVerificationInfolist
return $schema return $schema
->components([ ->components([
TextEntry::make('phone') TextEntry::make('phone')
->label(__('filament.fields.phone'))
->formatStateUsing(fn (string $state): string => format_phone($state)) ->formatStateUsing(fn (string $state): string => format_phone($state))
->copyable(), ->copyable(),
TextEntry::make('expires_at') TextEntry::make('expires_at')
->label(__('filament.fields.expires_at'))
->dateTime(), ->dateTime(),
IconEntry::make('verified') IconEntry::make('verified')
->label(__('filament.fields.verified'))
->boolean(), ->boolean(),
TextEntry::make('created_at') TextEntry::make('created_at')
->label(__('filament.fields.created_at'))
->dateTime(), ->dateTime(),
TextEntry::make('updated_at') TextEntry::make('updated_at')
->label(__('filament.fields.updated_at'))
->dateTime(), ->dateTime(),
]); ]);
} }

View File

@@ -16,22 +16,28 @@ class PhoneVerificationsTable
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->columns([ ->columns([
TextColumn::make('id') TextColumn::make('id')
->label(__('filament.fields.id'))
->sortable(), ->sortable(),
TextColumn::make('phone') TextColumn::make('phone')
->label(__('filament.fields.phone'))
->formatStateUsing(fn (string $state): string => format_phone($state)) ->formatStateUsing(fn (string $state): string => format_phone($state))
->searchable() ->searchable()
->sortable() ->sortable()
->copyable(), ->copyable(),
TextColumn::make('expires_at') TextColumn::make('expires_at')
->label(__('filament.fields.expires_at'))
->dateTime() ->dateTime()
->sortable(), ->sortable(),
IconColumn::make('verified') IconColumn::make('verified')
->label(__('filament.fields.verified'))
->boolean() ->boolean()
->sortable(), ->sortable(),
TextColumn::make('created_at') TextColumn::make('created_at')
->label(__('filament.fields.created_at'))
->dateTime() ->dateTime()
->sortable(), ->sortable(),
TextColumn::make('updated_at') TextColumn::make('updated_at')
->label(__('filament.fields.updated_at'))
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),

View File

@@ -13,12 +13,12 @@ class CreatedAtDateFilter
public static function make(): Filter public static function make(): Filter
{ {
return Filter::make('created_at') return Filter::make('created_at')
->label('Date') ->label(__('filament.filters.date'))
->schema([ ->schema([
DatePicker::make('from') DatePicker::make('from')
->label('From'), ->label(__('filament.filters.from')),
DatePicker::make('until') DatePicker::make('until')
->label('Until'), ->label(__('filament.filters.until')),
]) ])
->query(function (Builder $query, array $data): Builder { ->query(function (Builder $query, array $data): Builder {
return $query return $query
@@ -35,12 +35,16 @@ class CreatedAtDateFilter
$indicators = []; $indicators = [];
if ($data['from'] ?? null) { if ($data['from'] ?? null) {
$indicators[] = Indicator::make('From '.Carbon::parse($data['from'])->toFormattedDateString()) $indicators[] = Indicator::make(__('filament.filters.from_indicator', [
'date' => Carbon::parse($data['from'])->toFormattedDateString(),
]))
->removeField('from'); ->removeField('from');
} }
if ($data['until'] ?? null) { if ($data['until'] ?? null) {
$indicators[] = Indicator::make('Until '.Carbon::parse($data['until'])->toFormattedDateString()) $indicators[] = Indicator::make(__('filament.filters.until_indicator', [
'date' => Carbon::parse($data['until'])->toFormattedDateString(),
]))
->removeField('until'); ->removeField('until');
} }

View File

@@ -56,6 +56,57 @@ if (! function_exists('sendSMS')) {
} }
} }
if (! function_exists('sendBulkSMS')) {
/**
* Send bulk SMS
*
* @param array<int, array{phone: string, message: string}> $messages
*/
function sendBulkSMS(array $messages): bool
{
try {
$response = Http::timeout(config('services.sms.timeout'))
->connectTimeout(config('services.sms.connect_timeout'))
->retry(
times: 3,
sleepMilliseconds: 50,
throw: false,
when: function (Throwable $exception, PendingRequest $request): bool {
if ($exception instanceof ConnectionException) {
return true;
}
if ($exception instanceof RequestException) {
return $exception->response->serverError();
}
return false;
}
)
->post(config('services.sms.url').'/bulk', [
'messages' => $messages,
]);
if (! $response->successful()) {
Log::error('Bulk SMS API request failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return false;
}
return true;
} catch (Throwable $exception) {
Log::error('Bulk SMS API exception', [
'message' => $exception->getMessage(),
]);
return false;
}
}
}
if (! function_exists('unmask_phone')) { if (! function_exists('unmask_phone')) {
/** /**
* Unmask Turkmenistan phone number from TM code +993 6X XX XX XX to 6xxxxxxx * Unmask Turkmenistan phone number from TM code +993 6X XX XX XX to 6xxxxxxx

View File

@@ -1,81 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\SmsSendStatus;
use App\Models\Coupon;
use App\Models\SmsCampaign;
use App\Models\SmsSendLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\SerializesModels;
class SendSmsToCouponJob implements ShouldQueue
{
use Queueable;
use SerializesModels;
public int $tries = 3;
public function __construct(
public SmsCampaign $campaign,
public Coupon $coupon,
) {
$this->onQueue('sms');
}
public function handle(): void
{
$log = SmsSendLog::query()
->where('sms_campaign_id', $this->campaign->id)
->where('coupon_id', $this->coupon->id)
->first();
if ($log === null) {
return;
}
if (! is_valid_coupon_phone($this->coupon->phone)) {
$this->markSkipped($log);
return;
}
$success = sendSMS($this->coupon->phone, $this->campaign->message);
$log->update([
'status' => $success ? SmsSendStatus::Sent : SmsSendStatus::Failed,
'error_message' => $success ? null : 'SMS API request failed',
'attempted_at' => now(),
]);
$this->campaign->increment($success ? 'sent_count' : 'failed_count');
$this->markCampaignCompleteIfFinished();
usleep((int) config('services.sms.bulk_delay_ms', 300) * 1000);
}
protected function markSkipped(SmsSendLog $log): void
{
if ($log->status === SmsSendStatus::SkippedInvalid) {
return;
}
$log->update([
'status' => SmsSendStatus::SkippedInvalid,
'attempted_at' => now(),
]);
$this->campaign->increment('skipped_count');
$this->markCampaignCompleteIfFinished();
}
protected function markCampaignCompleteIfFinished(): void
{
$this->campaign->refresh();
if ($this->campaign->processedCount() >= $this->campaign->recipient_count) {
$this->campaign->update(['completed_at' => now()]);
}
}
}

View File

@@ -2,15 +2,8 @@
namespace App\Services; namespace App\Services;
use App\Enums\SmsSendStatus;
use App\Jobs\SendSmsToCouponJob;
use App\Models\Coupon; use App\Models\Coupon;
use App\Models\SmsCampaign;
use App\Models\SmsSendLog;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
class SmsBroadcastService class SmsBroadcastService
{ {
@@ -36,62 +29,4 @@ class SmsBroadcastService
return $query->orderBy('id')->get(); return $query->orderBy('id')->get();
} }
public function startCampaign(
User $admin,
string $message,
bool $sendToAll,
array $selectedIds = [],
array $excludedIds = [],
): SmsCampaign {
$recipients = $this->resolveRecipients($sendToAll, $selectedIds, $excludedIds);
$mode = $sendToAll ? 'all' : 'selected';
return DB::transaction(function () use ($admin, $message, $mode, $recipients): SmsCampaign {
$campaign = SmsCampaign::query()->create([
'message' => $message,
'mode' => $mode,
'recipient_count' => $recipients->count(),
'created_by' => $admin->id,
]);
$jobs = [];
foreach ($recipients as $coupon) {
if (! is_valid_coupon_phone($coupon->phone)) {
$this->createLog($campaign, $coupon, SmsSendStatus::SkippedInvalid);
$campaign->increment('skipped_count');
continue;
}
$this->createLog($campaign, $coupon, SmsSendStatus::Pending);
$jobs[] = new SendSmsToCouponJob($campaign, $coupon);
}
if ($jobs === []) {
$campaign->update(['completed_at' => now()]);
return $campaign->fresh();
}
Bus::chain($jobs)
->onQueue('sms')
->dispatch();
return $campaign->fresh();
});
}
protected function createLog(SmsCampaign $campaign, Coupon $coupon, SmsSendStatus $status): SmsSendLog
{
return SmsSendLog::query()->create([
'sms_campaign_id' => $campaign->id,
'coupon_id' => $coupon->id,
'phone' => $coupon->phone,
'message' => $campaign->message,
'status' => $status,
'attempted_at' => $status === SmsSendStatus::Pending ? null : now(),
]);
}
} }

View File

@@ -55,11 +55,16 @@ class SmsMessageAnalyzer
$parts = $this->estimatedParts($message); $parts = $this->estimatedParts($message);
if ($parts === 0) { if ($parts === 0) {
return '0 characters'; return __('filament.sms_analyzer.zero_characters');
} }
$partLabel = $parts === 1 ? '1 SMS' : "{$parts} SMS"; $smsLabel = $parts === 1
? __('filament.sms_analyzer.one_sms')
: __('filament.sms_analyzer.multiple_sms', ['count' => $parts]);
return "{$characters} characters · {$partLabel}"; return __('filament.sms_analyzer.characters_sms', [
'characters' => $characters,
'sms' => $smsLabel,
]);
} }
} }

9
lang/tk/auth.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
return [
'failed' => 'Bu maglumatlar biziň ýazgylarymyza gabat gelenok.',
'password' => 'Berlen parol nädogry.',
'throttle' => 'Giriş synanyşyklary köp. :seconds sekuntdan soň gaýtadan synanyşyň.',
];

74
lang/tk/filament.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
return [
'coupons' => [
'navigation' => 'Kuponlar',
],
'phone_verifications' => [
'navigation' => 'OTP tassyklamalary',
'model' => 'OTP tassyklama',
'plural' => 'OTP tassyklamalary',
],
'send_sms' => [
'navigation' => 'SMS ibermek',
'title' => 'SMS ibermek',
'sections' => [
'message' => 'Hat',
'message_description' => 'SMS-ni alýanlar görmeli bolşy ýaly takyk ýazyň.',
'recipients' => 'Alyjylar',
'recipients_description' => 'Hemmä ibermek, belli kupon eýelerini saýlamak ýa-da sanlary aýyrmak.',
],
'fields' => [
'message' => 'SMS habary',
'send_to_all' => 'Ähli kupon eýelerine ibermek',
'send_to_all_helper' => 'Açyk bolsa, ähli kupon eýeleri girýär. El bilen saýlaw öçürilýär.',
'coupon_ids' => 'Alyjylary saýlaň',
'coupon_ids_helper' => 'Telefon, kupon kody ýa-da ID boýunça gözleg.',
'excluded_coupon_ids' => 'Alyjylary aýyrmak',
'excluded_coupon_ids_helper' => 'Aýrylan sanlar bu habary almaz, hatda ähläne iberilende hem.',
],
'actions' => [
'send' => 'SMS ibermek',
'send_confirm_heading' => 'SMS häzir ibermeli?',
'send_confirm_submit' => 'Iber',
],
'notifications' => [
'no_valid_recipients' => 'Dogry alyjy ýok',
'no_valid_recipients_body' => 'Saýlawy ýa-da aýyrmalary üýtgediň — dogry telefon belgili kupon eýesi bu habary almaz.',
'submitted' => 'SMS kampaniýasy iberildi',
'submitted_body' => ':count alyja iberildi.',
'failed' => 'SMS ibermek başa barmady',
'failed_body' => 'Näsazlyk ýüze çykdy. Täzeden synanyşyň.',
],
'confirmation' => ':count alyja ibermeli? :summary',
],
'filters' => [
'date' => 'Sene',
'from' => 'Başlanýan',
'until' => 'Gutarýan',
'from_indicator' => 'Başlanýan :date',
'until_indicator' => 'Gutarýan :date',
],
'sms_analyzer' => [
'zero_characters' => '0 harp',
'characters_sms' => ':characters harp · :sms',
'one_sms' => '1 SMS',
'multiple_sms' => ':count SMS',
],
'fields' => [
'id' => 'ID',
'phone' => 'Telefon',
'code' => 'Kupon kody',
'created_at' => 'Döredilen',
'updated_at' => 'Üýtgedilen',
'expires_at' => 'Gutarýan wagty',
'verified' => 'Tassyklanan',
],
];

34
lang/tk/validation.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
return [
'accepted' => ':attribute kabul edilmeli.',
'array' => ':attribute massiw bolmaly.',
'boolean' => ':attribute dogry ýa-da ýalňyş bolmaly.',
'confirmed' => ':attribute tassyklamasy gabat gelenok.',
'email' => ':attribute dogry e-poçta salgysy bolmaly.',
'exists' => 'Saýlanan :attribute nädogry.',
'integer' => ':attribute san bolmaly.',
'max' => [
'array' => ':attribute iň köp :max elementden ybarat bolmaly.',
'numeric' => ':attribute iň köp :max bolmaly.',
'string' => ':attribute iň köp :max harp bolmaly.',
],
'min' => [
'array' => ':attribute iň az :min elementden ybarat bolmaly.',
'numeric' => ':attribute iň az :min bolmaly.',
'string' => ':attribute iň az :min harp bolmaly.',
],
'required' => ':attribute meýdany hökmany.',
'string' => ':attribute setir bolmaly.',
'attributes' => [
'email' => 'e-poçta salgysy',
'password' => 'parol',
'message' => 'SMS habary',
'send_to_all' => 'ähli kupon eýelerine ibermek',
'coupon_ids' => 'alyjylar',
'excluded_coupon_ids' => 'aýrylan alyjylar',
],
];

View File

@@ -0,0 +1,28 @@
<?php
return [
'single' => [
'label' => 'İlişkilendir',
'modal' => [
'heading' => ':label İlişkilendir',
'fields' => [
'record_id' => [
'label' => 'Kayıt',
],
],
'actions' => [
'associate' => [
'label' => 'İlişkilendir',
],
'associate_another' => [
'label' => 'İlişkilendir ve başka bir taneye başla',
],
],
],
'notifications' => [
'associated' => [
'title' => 'İlişkilendirildi',
],
],
],
];

View File

@@ -0,0 +1,28 @@
<?php
return [
'single' => [
'label' => 'İliştir',
'modal' => [
'heading' => ':label iliştir',
'fields' => [
'record_id' => [
'label' => 'Kayıt',
],
],
'actions' => [
'attach' => [
'label' => 'İliştir',
],
'attach_another' => [
'label' => 'İliştir ve başka bir taneye başla',
],
],
],
'notifications' => [
'attached' => [
'title' => 'İliştirildi',
],
],
],
];

View File

@@ -0,0 +1,23 @@
<?php
return [
'single' => [
'label' => ':label Oluştur',
'modal' => [
'heading' => ':label oluştur',
'actions' => [
'create' => [
'label' => 'Döret',
],
'create_another' => [
'label' => 'Oluştur & yeni oluştur',
],
],
],
'notifications' => [
'created' => [
'title' => 'Oluşturuldu',
],
],
],
];

View File

@@ -0,0 +1,73 @@
<?php
return [
'single' => [
'label' => 'Poz',
'modal' => [
'heading' => ':label poz',
'actions' => [
'delete' => [
'label' => 'Poz',
],
],
],
'notifications' => [
'deleted' => [
'title' => 'Pozuldy',
],
],
],
'multiple' => [
'label' => 'Saýlananlary poz',
'modal' => [
'heading' => 'Saýlananlary poz',
'actions' => [
'delete' => [
'label' => 'Poz',
],
],
],
'notifications' => [
'deleted' => [
'title' => 'Pozuldy',
],
'deleted_partial' => [
'title' => ':total ýazgydan :count pozuldy',
'missing_authorization_failure_message' => ':count ýazgy pozmak üçin ygtyýaryňyz ýok.',
'missing_processing_failure_message' => ':count ýazgy pozulmady.',
],
'deleted_none' => [
'title' => 'Ýazgylar pozulmady',
'missing_authorization_failure_message' => ':count ýazgy pozmak üçin ygtyýaryňyz ýok.',
'missing_processing_failure_message' => ':count ýazgy pozulmady.',
],
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
return [
'single' => [
'label' => 'Ayır',
'modal' => [
'heading' => ':label ayır',
'actions' => [
'detach' => [
'label' => 'Ayır',
],
],
],
'notifications' => [
'detached' => [
'title' => 'Ayrıldı',
],
],
],
'multiple' => [
'label' => 'Seçiliyi ayır',
'modal' => [
'heading' => ':label seçiliyi ayır ',
'actions' => [
'detach' => [
'label' => 'Seçiliyi ayır',
],
],
],
'notifications' => [
'detached' => [
'title' => 'Ayrıldı',
],
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
return [
'single' => [
'label' => 'Ayrıştır',
'modal' => [
'heading' => ':label ayrıştır',
'actions' => [
'dissociate' => [
'label' => 'Ayrıştır',
],
],
],
'notifications' => [
'dissociated' => [
'title' => 'Ayrıştırıldı',
],
],
],
'multiple' => [
'label' => 'Seçiliyi ayrıştır',
'modal' => [
'heading' => ':label seçiliyi ayrıştır',
'actions' => [
'dissociate' => [
'label' => 'Seçiliyi ayrıştır',
],
],
],
'notifications' => [
'dissociated' => [
'title' => 'Ayrıştırıldı',
],
],
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'single' => [
'label' => 'Üýtget',
'modal' => [
'heading' => ':label üýtget',
'actions' => [
'save' => [
'label' => 'Sakla',
],
],
],
'notifications' => [
'saved' => [
'title' => 'Kaydedildi',
],
],
],
];

View File

@@ -0,0 +1,48 @@
<?php
return [
'label' => 'Dışa Aktar :label',
'modal' => [
'heading' => 'Dışa Aktar :label',
'form' => [
'columns' => [
'label' => 'Sütünler',
'form' => [
'is_enabled' => [
'label' => ':column etkin',
],
'label' => [
'label' => ':column etiketi',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Dışa Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Dışa Aktarım Tamamlandı',
'actions' => [
'download_csv' => [
'label' => '.csv Olarak İndir',
],
'download_xlsx' => [
'label' => '.xlsx Olarak İndir',
],
],
],
'max_rows' => [
'title' => 'Maksimum Satır Sayısııldı',
'body' => 'Birden fazla satırı dışa aktaramazsınız.|:count satırı dışa aktaramazsınız.',
],
'started' => [
'title' => 'Dışa Aktarım Başladı',
'body' => 'Dışa aktarım başladı ve 1 satır arka planda işlenecek.|Dışa aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'file_name' => 'export-:export_id-:model',
];

View File

@@ -0,0 +1,46 @@
<?php
return [
'single' => [
'label' => 'Kalıcı olarak sil',
'modal' => [
'heading' => ':label kalıcı olarak sil',
'actions' => [
'delete' => [
'label' => 'Kalıcı olarak sil',
],
],
],
'notifications' => [
'deleted' => [
'title' => 'Kayıt kalıcı olarak silindi',
],
],
],
'multiple' => [
'label' => 'Seçiliyi kalıcı olarak sil',
'modal' => [
'heading' => ':label seçiliyi kalıcı olarak sil',
'actions' => [
'delete' => [
'label' => 'Kalıcı olarak sil',
],
],
],
'notifications' => [
'deleted' => [
'title' => 'Kayıtlar kalıcı olarak silindi',
],
'deleted_partial' => [
'title' => ':total kayıttan :count kayıt kalıcı olarak silindi',
'missing_authorization_failure_message' => ':count kayıtı kalıcı olarak silmek için gereken izniniz yok.',
'missing_processing_failure_message' => ':count kayıt kalıcı olarak silinemedi.',
],
'deleted_none' => [
'title' => 'Kayıtlar kalıcı olarak silinemedi',
'missing_authorization_failure_message' => ':count kayıtı kalıcı olarak silmek için gereken izniniz yok.',
'missing_processing_failure_message' => ':count kayıt kalıcı olarak silinemedi.',
],
],
],
];

View File

@@ -0,0 +1,7 @@
<?php
return [
'trigger' => [
'label' => 'Eylemler',
],
];

View File

@@ -0,0 +1,56 @@
<?php
return [
'label' => 'İçe Aktar: :label',
'modal' => [
'heading' => 'İçe Aktar: :label',
'form' => [
'file' => [
'label' => 'Dosya',
'placeholder' => 'Bir CSV dosyası seçin',
'rules' => [
'duplicate_columns' => '{0} Dosya birden fazla boş sütun başlığı içeremez.|{1,*} Dosya tekrar eden sütun başlığı içeremez: :columns.',
],
],
'columns' => [
'label' => 'Sütünler',
'placeholder' => 'Sütunları eşleştirin',
],
],
'actions' => [
'download_example' => [
'label' => 'Örnek CSV Dosyasını İndir',
],
'import' => [
'label' => 'İçe Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'İçe Aktarım Tamamlandı',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Başarısız satır hakkında bilgileri indir|Başarısız satırlar hakkında bilgileri indir',
],
],
],
'max_rows' => [
'title' => 'Yüklenen Dosya Çok Büyük',
'body' => 'Aynı anda 1\'den fazla satır içeren dosyaları içe aktaramazsınız.|Aynı anda :count\'den fazla satır içeren dosyaları içe aktaramazsınız.',
],
'started' => [
'title' => 'İçe Aktarım Başladı',
'body' => 'İçe aktarım başladı ve 1 satır arka planda işlenecek.|İçe aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'error',
'system_error' => 'Sistem Hatası',
'column_mapping_required_for_new_record' => ':attribute sütunu dosyadaki bir sütun ile eşleştirilmedi, fakat bu sütun yeni kayıt oluşturmak için gerekli bir sütun.',
],
];

View File

@@ -0,0 +1,16 @@
<?php
return [
'confirmation' => 'Bunu yapmak istediğinizden emin misiniz?',
'actions' => [
'cancel' => [
'label' => 'Ýatyr',
],
'confirm' => [
'label' => 'Tassykla',
],
'submit' => [
'label' => 'Iber',
],
],
];

View File

@@ -0,0 +1,8 @@
<?php
return [
'throttled' => [
'title' => 'Çok Fazla Deneme Yapıldı',
'body' => 'Çok fazla deneme yapıldı, lütfen :seconds saniye sonra tekrar deneyin.',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'single' => [
'label' => 'Çoğalt',
'modal' => [
'heading' => ':label çoğalt',
'actions' => [
'replicate' => [
'label' => 'Çoğalt',
],
],
],
'notifications' => [
'replicated' => [
'title' => 'Kayıt çoğaltıldı',
],
],
],
];

View File

@@ -0,0 +1,46 @@
<?php
return [
'single' => [
'label' => 'Geri yükle',
'modal' => [
'heading' => ':label geri yükle',
'actions' => [
'restore' => [
'label' => 'Geri yükle',
],
],
],
'notifications' => [
'restored' => [
'title' => 'Kayıt geri yüklendi',
],
],
],
'multiple' => [
'label' => 'Seçileni geri yükle',
'modal' => [
'heading' => ':label seçileni geri yükle',
'actions' => [
'restore' => [
'label' => 'Geri yükle',
],
],
],
'notifications' => [
'restored' => [
'title' => 'Kayıtlar geri yüklendi',
],
'restored_partial' => [
'title' => ':total kayıttan :count kayıt geri yüklendi',
'missing_authorization_failure_message' => ':count kayıt geri yüklemek için gereken izniniz yok.',
'missing_processing_failure_message' => ':count geri yüklenemedi.',
],
'restored_none' => [
'title' => 'Kayıtlar geri yüklenemedi',
'missing_authorization_failure_message' => ':count kayıt geri yüklemek için gereken izniniz yok.',
'missing_processing_failure_message' => ':count geri yüklenemedi.',
],
],
],
];

View File

@@ -0,0 +1,15 @@
<?php
return [
'single' => [
'label' => 'Gör',
'modal' => [
'heading' => ':label gör',
'actions' => [
'close' => [
'label' => 'Ýap',
],
],
],
],
];

View File

@@ -0,0 +1,498 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Klonla',
],
'add' => [
'label' => ':label\'e Ekle',
'modal' => [
'heading' => ':label\'e Ekle',
'actions' => [
'add' => [
'label' => 'Goş',
],
],
],
],
'add_between' => [
'label' => 'Bloklar arasına ekle',
'modal' => [
'heading' => ':label\'e Ekle',
'actions' => [
'add' => [
'label' => 'Goş',
],
],
],
],
'delete' => [
'label' => 'Poz',
],
'edit' => [
'label' => 'Üýtget',
'modal' => [
'heading' => 'Bloğu Düzenle',
'actions' => [
'save' => [
'label' => 'Değişiklikleri Kaydet',
],
],
],
],
'reorder' => [
'label' => 'Taşı',
],
'move_down' => [
'label' => 'Aşağı taşı',
],
'move_up' => [
'label' => 'Yukarı taşı',
],
'collapse' => [
'label' => 'Daralt',
],
'expand' => [
'label' => 'Genişlet',
],
'collapse_all' => [
'label' => 'Tümünü daralt',
],
'expand_all' => [
'label' => 'Tümünü genişlet',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Tüm seçimi kaldır',
],
'select_all' => [
'label' => 'Hemmesini saýla',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'Ýatyr',
],
'drag_crop' => [
'label' => 'Sürükleme modu "kırpma"',
],
'drag_move' => [
'label' => 'Sürükleme modu "taşıma"',
],
'flip_horizontal' => [
'label' => 'Görüntüyü yatay olarak çevir',
],
'flip_vertical' => [
'label' => 'Görüntüyü dikey olarak çevir',
],
'move_down' => [
'label' => 'Görüntüyü aşağı taşı',
],
'move_left' => [
'label' => 'Görüntüyü sola taşı',
],
'move_right' => [
'label' => 'Görüntüyü sağa taşı',
],
'move_up' => [
'label' => 'Görüntüyü yukarı taşı',
],
'reset' => [
'label' => 'Täzeden',
],
'rotate_left' => [
'label' => 'Görüntüyü sola döndür',
],
'rotate_right' => [
'label' => 'Görüntüyü sağa döndür',
],
'set_aspect_ratio' => [
'label' => 'En boy oranını :ratio olarak ayarla',
],
'save' => [
'label' => 'Sakla',
],
'zoom_100' => [
'label' => 'Görüntüyü %100 yakınlaştır',
],
'zoom_in' => [
'label' => 'Yakınlaştır',
],
'zoom_out' => [
'label' => 'Uzaklaştır',
],
],
'fields' => [
'height' => [
'label' => 'Yükseklik',
'unit' => 'px',
],
'rotation' => [
'label' => 'Döndürme',
'unit' => '°',
],
'width' => [
'label' => 'Genişlik',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'En boy oranı',
'no_fixed' => [
'label' => 'Serbest',
],
],
'svg' => [
'messages' => [
'confirmation' => 'SVG dosyalarını düzenleme, ölçeklendirme yaptığınızda kalite kaybına neden olabileceği için tavsiye edilmez.\\n Devam etmek istediğinize emin misiniz?',
'disabled' => 'SVG dosyalarını düzenleme, ölçeklendirme yaptığınızda kalite kaybına neden olduğu için engellendi.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Satır ekle',
],
'delete' => [
'label' => 'Satır sil',
],
'reorder' => [
'label' => 'Satır sırala',
],
],
'fields' => [
'key' => [
'label' => 'Anahtar',
],
'value' => [
'label' => 'Değer',
],
],
],
'markdown_editor' => [
'file_attachments_accepted_file_types_message' => 'Yüklenen dosyalar şu türlerden olmalıdır: :values.',
'file_attachments_max_size_message' => 'Yüklenen dosyalar :max kilobayttan büyük olmamalıdır.',
'tools' => [
'attach_files' => 'Dosya ekle',
'blockquote' => 'Alıntı',
'bold' => 'Kalın',
'bullet_list' => 'Sanaw',
'code_block' => 'Kod bloğu',
'heading' => 'Başlık',
'italic' => 'Eğik',
'link' => 'Bağlantı',
'ordered_list' => 'Numaralı liste',
'redo' => 'Yinele',
'strike' => 'Üstü çizili',
'table' => 'Tablo',
'undo' => 'Geri al',
],
],
'modal_table_select' => [
'actions' => [
'select' => [
'label' => 'Saýla',
'actions' => [
'select' => [
'label' => 'Saýla',
],
],
],
],
],
'radio' => [
'boolean' => [
'true' => 'Hawa',
'false' => 'Ýok',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => ':label\'e ekle',
],
'add_between' => [
'label' => 'Arasına yerleştir',
],
'delete' => [
'label' => 'Poz',
],
'clone' => [
'label' => 'Klonla',
],
'reorder' => [
'label' => 'Taşı',
],
'move_down' => [
'label' => 'Aşağı taşı',
],
'move_up' => [
'label' => 'Yukarı taşı',
],
'collapse' => [
'label' => 'Daralt',
],
'expand' => [
'label' => 'Genişlet',
],
'collapse_all' => [
'label' => 'Tümünü daralt',
],
'expand_all' => [
'label' => 'Tümünü genişlet',
],
],
],
'rich_editor' => [
'actions' => [
'attach_files' => [
'label' => 'Dosya yükle',
'modal' => [
'heading' => 'Dosya yükle',
'form' => [
'file' => [
'label' => [
'new' => 'Dosya',
'existing' => 'Dosyayı değiştir',
],
],
'alt' => [
'label' => [
'new' => 'Açıklama metni',
'existing' => 'Açıklama metnini değiştir',
],
],
],
],
],
'custom_block' => [
'modal' => [
'actions' => [
'insert' => [
'label' => 'Goş',
],
'save' => [
'label' => 'Sakla',
],
],
],
],
'grid' => [
'label' => 'Grid',
'modal' => [
'heading' => 'Grid',
'form' => [
'preset' => [
'label' => 'Ön ayar',
'placeholder' => 'Yok',
'options' => [
'two' => 'İki',
'three' => 'Üç',
'four' => 'Dört',
'five' => 'Beş',
'two_start_third' => 'İki (Başta Üçte Bir)',
'two_end_third' => 'İki (Sonda Üçte Bir)',
'two_start_fourth' => 'İki (Başta Dörtte Bir)',
'two_end_fourth' => 'İki (Sonda Dörtte Bir)',
],
],
'columns' => [
'label' => 'Sütünler',
],
'from_breakpoint' => [
'label' => 'Kesme noktasından',
'options' => [
'default' => 'Hemmesi',
'sm' => 'Küçük (sm)',
'md' => 'Orta (md)',
'lg' => 'Büyük (lg)',
'xl' => 'Çok büyük (xl)',
'2xl' => 'İki kat büyük (2xl)',
],
],
'is_asymmetric' => [
'label' => 'İki asimetrik sütun',
],
'start_span' => [
'label' => 'Başlangıç aralığı',
],
'end_span' => [
'label' => 'Bitiş aralığı',
],
],
],
],
'link' => [
'label' => 'Üýtget',
'modal' => [
'heading' => 'Bağlantı',
'form' => [
'url' => [
'label' => 'URL',
],
'should_open_in_new_tab' => [
'label' => 'Yeni sekmede aç',
],
],
],
],
'text_color' => [
'label' => 'Yazı rengi',
'modal' => [
'heading' => 'Yazı rengi',
'form' => [
'color' => [
'label' => 'Renk',
],
'custom_color' => [
'label' => 'Özel renk',
],
],
],
],
],
'file_attachments_accepted_file_types_message' => 'Yüklenen dosyalar şu türlerden olmalıdır: :values.',
'file_attachments_max_size_message' => 'Yüklenen dosyalar :max kilobayttan büyük olmamalıdır.',
'no_merge_tag_search_results_message' => 'Uygun birleşme etiketi bulunamadı.',
'mentions' => [
'no_options_message' => 'Seçenek bulunamadı.',
'no_search_results_message' => 'Aramanızla eşleşen sonuç bulunamadı.',
'search_prompt' => 'Aramak için yazmaya başlayın...',
'searching_message' => 'Aranıyor...',
],
'tools' => [
'align_center' => 'Ortaya hizala',
'align_end' => 'Sona hizala',
'align_justify' => 'İki yana yasla',
'align_start' => 'Başa hizala',
'attach_files' => 'Dosya ekle',
'blockquote' => 'Alıntı',
'bold' => 'Kalın',
'bullet_list' => 'Sırasız liste',
'clear_formatting' => 'Biçimlendirmeyi temizle',
'code' => 'Kod',
'code_block' => 'Kod bloğu',
'custom_blocks' => 'Bloklar',
'details' => 'Jikme-jik',
'h1' => 'Başlık',
'h2' => 'Başlık 2',
'h3' => 'Alt başlık',
'grid' => 'Grid',
'grid_delete' => 'Grid\'i sil',
'highlight' => 'Vurgula',
'horizontal_rule' => 'Yatay çizgi',
'italic' => 'Eğik',
'lead' => 'Öne çıkan metin',
'link' => 'Bağlantı',
'merge_tags' => 'Birleşme etiketleri',
'ordered_list' => 'Sıralı liste',
'redo' => 'Yinele',
'small' => 'Küçük metin',
'strike' => 'Üstü çizili',
'subscript' => 'Alt simge',
'superscript' => 'Üst simge',
'table' => 'Tablo',
'table_delete' => 'Tabloyu sil',
'table_add_column_before' => 'Öncesine sütun ekle',
'table_add_column_after' => 'Sonrasına sütun ekle',
'table_delete_column' => 'Sütunu sil',
'table_add_row_before' => 'Üstüne satır ekle',
'table_add_row_after' => 'Altına satır ekle',
'table_delete_row' => 'Satırı sil',
'table_merge_cells' => 'Hücreleri birleştir',
'table_split_cell' => 'Hücreyi böl',
'table_toggle_header_row' => 'Başlık satırını aç/kapat',
'table_toggle_header_cell' => 'Başlık hücresini aç/kapat',
'text_color' => 'Yazı rengi',
'underline' => 'Altı çizili',
'undo' => 'Geri al',
],
'uploading_file_message' => 'Dosya yükleniyor...',
],
'select' => [
'actions' => [
'create_option' => [
'label' => 'Döret',
'modal' => [
'heading' => 'Döret',
'actions' => [
'create' => [
'label' => 'Döret',
],
'create_another' => [
'label' => 'Oluştur & Yeni oluştur',
],
],
],
],
'edit_option' => [
'label' => 'Üýtget',
'modal' => [
'heading' => 'Üýtget',
'actions' => [
'save' => [
'label' => 'Sakla',
],
],
],
],
],
'boolean' => [
'true' => 'Hawa',
'false' => 'Ýok',
],
'loading_message' => 'Ýüklenýär...',
'max_items_message' => 'Sadece :count adet seçilebilir.',
'no_options_message' => 'Seçenek bulunamadı.',
'no_search_results_message' => 'Arama kriterlerinize uyan seçenek yok.',
'placeholder' => 'Bir seçenek seçin',
'searching_message' => 'Aranıyor...',
'search_prompt' => 'Aramak için yazmaya başlayın...',
],
'tags_input' => [
'actions' => [
'delete' => [
'label' => 'Poz',
],
],
'placeholder' => 'Yeni etiket',
],
'text_input' => [
'actions' => [
'copy' => [
'label' => 'Göçür',
'message' => 'Göçürildi',
],
'hide_password' => [
'label' => 'Şifreyi gizle',
],
'show_password' => [
'label' => 'Şifreyi göster',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Hawa',
'false' => 'Ýok',
],
],
];

View File

@@ -0,0 +1,8 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'En az 1 adet :attribute alanı seçmelisiniz.',
'only_one_must_be_selected' => 'Sadece 1 adet :attribute alanı seçilmelidir.',
],
];

View File

@@ -0,0 +1,24 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count kayıt az göster',
'expand_list' => ':count kayıt daha göster',
],
'more_list_items' => 've :count kayıt daha',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Anahtar',
],
'value' => [
'label' => 'Değer',
],
],
'placeholder' => 'Kayıt yok',
],
],
];

View File

@@ -0,0 +1,19 @@
<?php
return [
'modal' => [
'heading' => 'Habarnamalar',
'actions' => [
'clear' => [
'label' => 'Arassala',
],
'mark_all_as_read' => [
'label' => 'Tümünü okundu işaretle',
],
],
'empty' => [
'heading' => 'Bildirim yok',
'description' => 'Lütfen sonra kontrol ediniz',
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'notifications' => [
'blocked' => [
'title' => 'E-posta adresi güncelleme isteği engellendi',
'body' => 'Başarılı bir şekilde E-posta adresinizin :email olarak güncellenme isteğini engellediniz. Eğer bu isteği siz yapmadıysanız lütfen bizimle iletişime geçin.',
],
'failed' => [
'title' => 'E-posta adresi güncelleme isteği engellenirken bir hata oluştu',
'body' => 'Ne yazık ki, E-posta adresinizin :email olarak güncellenme isteğini engelleme işleminiz başarısız oldu, siz engelleyemeden E-posta adresi onaylandı. Eğer bu isteği siz yapmadıysanız lütfen bizimle iletişime geçin.',
],
],
];

View File

@@ -0,0 +1,10 @@
<?php
return [
'notifications' => [
'verified' => [
'title' => 'E-posta adresi güncellendi',
'body' => 'E-posta adresiniz başarıyla :email olarak güncellendi.',
],
],
];

View File

@@ -0,0 +1,40 @@
<?php
return [
'label' => 'Ýap',
'modal' => [
'heading' => 'Doğrulama uygulamasını devre dışı bırak',
'description' => 'Doğrulama uygulamasını devre dışı bırakmak istediğinize emin misiniz? Bunu devre dışı bırakmak hesabınızda bulunan ekstra koruma katmanını kaldıracaktır.',
'form' => [
'code' => [
'label' => 'Doğrulama uygulamanızdaki 6 haneli kodu girin',
'validation_attribute' => 'kod',
'actions' => [
'use_recovery_code' => [
'label' => 'Bunun yerine kurtarma kodu girin',
],
],
'messages' => [
'invalid' => 'Girmiş olduğunuz kod geçersiz.',
],
],
'recovery_code' => [
'label' => 'Veya, bir kurtarma kodu girin',
'validation_attribute' => 'kurtarma kodu',
'messages' => [
'invalid' => 'Girmiş olduğunuz kurtarma kodu geçersiz.',
],
],
],
'actions' => [
'submit' => [
'label' => 'Uygulamayı devre dışı bırak',
],
],
],
'notifications' => [
'disabled' => [
'title' => 'Doğrulama uygulaması devre dışı bırakıldı',
],
],
];

View File

@@ -0,0 +1,43 @@
<?php
return [
'label' => 'Kurtarma kodlarını yeniden oluştur',
'modal' => [
'heading' => 'Kurtarma kodlarını yeniden oluştur',
'description' => 'Eğer kurtarma kodlarınızı kaybederseniz buradan yeniden oluşturabilirsiniz. Eski kodlarınız devre dışı kalacaktır.',
'form' => [
'code' => [
'label' => 'Doğrulama uygulamanızdaki 6 haneli kodu girin',
'validation_attribute' => 'kod',
'messages' => [
'invalid' => 'Girmiş olduğunuz kod geçersiz.',
],
],
'password' => [
'label' => 'Veya, geçerli şifrenizi girin',
'validation_attribute' => 'şifre',
],
],
'actions' => [
'submit' => [
'label' => 'Kodları yeniden oluştur',
],
],
],
'notifications' => [
'regenerated' => [
'title' => 'Yeni kurtarma kodları oluşturuldu',
],
],
'show_new_recovery_codes' => [
'modal' => [
'heading' => 'Yeni kodlar',
'description' => 'Lütfen bu kodları güvenli bir şekilde saklayın. Bu kodlar size sadece bir kere gösterilecek ve eğer doğrulama uygulamanıza erişiminizi kaybederseniz bu kodları kullanmanız gerekecek:',
'actions' => [
'submit' => [
'label' => 'Ýap',
],
],
],
],
];

View File

@@ -0,0 +1,44 @@
<?php
return [
'label' => 'Kur',
'modal' => [
'heading' => 'Doğrulama uygulaması kur',
'description' => 'Devam etmek için Google Authenticator gibi (<x-filament::link href="https://itunes.apple.com/us/app/google-authenticator/id388497605" target="_blank">iOS</x-filament::link>, <x-filament::link href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Android</x-filament::link>) uygulamalardan birine ihtiyacınız olacak.',
'content' => [
'qr_code' => [
'instruction' => 'Doğrulama uygulamanızla aşağıdaki QR kodunu taratın:',
'alt' => 'QR kodunu taratın',
],
'text_code' => [
'instruction' => 'Veya aşağıdaki kodu elle girin:',
'messages' => [
'copied' => 'Göçürildi',
],
],
'recovery_codes' => [
'instruction' => 'Lütfen bu kodları güvenli bir şekilde saklayın. Bu kodlar size sadece bir kere gösterilecek ve eğer doğrulama uygulamanıza erişiminizi kaybederseniz bu kodları kullanmanız gerekecek:',
],
],
'form' => [
'code' => [
'label' => 'Doğrulama uygulamanızdaki 6 haneli kodu girin',
'validation_attribute' => 'kod',
'below_content' => 'Giriş yaparken veya hassas bir işlem gerçekleştirirken doğrulama uygulamanız tarafından oluşturulan 6 haneli kodu girmeniz gerekecek.',
'messages' => [
'invalid' => 'Girmiş olduğunuz kod geçersiz.',
],
],
],
'actions' => [
'submit' => [
'label' => 'Doğrulama uygulamasını etkinleştir',
],
],
],
'notifications' => [
'enabled' => [
'title' => 'Doğrulama uygulaması etkinleştirildi',
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
return [
'management_schema' => [
'actions' => [
'label' => 'Doğrulama uygulaması',
'below_content' => 'Girişinizi doğrulamak için doğrulama uygulamanız tarafından oluşturulan kodları kullanın',
'messages' => [
'enabled' => 'Etkin',
'disabled' => 'Devre Dışı',
],
],
],
'login_form' => [
'label' => 'Use a code from your authenticator app',
'code' => [
'label' => 'Girişinizi doğrulamak için doğrulama uygulamanız tarafından oluşturulan bir kod girin',
'validation_attribute' => 'kod',
'actions' => [
'use_recovery_code' => [
'label' => 'Kurtarma kodu kullan',
],
],
'messages' => [
'invalid' => 'Girmiş olduğunuz kod geçersiz.',
],
],
'recovery_code' => [
'label' => 'Veya kurtarma kodu girin',
'validation_attribute' => 'kurtarma kodu',
'messages' => [
'invalid' => 'Girmiş olduğunuz kurtarma kodu geçersiz.',
],
],
],
];

View File

@@ -0,0 +1,38 @@
<?php
return [
'label' => 'Ýap',
'modal' => [
'heading' => 'E-posta doğrulama kodlarını devre dışı bırak',
'description' => 'E-posta doğrulama kodları almayı durdurmak istediğinizden emin misiniz? Bu özelliği devre dışı bırakmak hesabınızdan ek bir güvenlik katmanını kaldıracaktır.',
'form' => [
'code' => [
'label' => 'Size e-posta ile gönderdiğimiz 6 haneli kodu girin',
'validation_attribute' => 'kod',
'actions' => [
'resend' => [
'label' => 'E-posta ile yeni kod gönder',
'notifications' => [
'resent' => [
'title' => 'Size e-posta ile yeni bir kod gönderdik',
],
],
],
],
'messages' => [
'invalid' => 'Girdiğiniz kod geçersiz.',
],
],
],
'actions' => [
'submit' => [
'label' => 'E-posta doğrulama kodlarını devre dışı bırak',
],
],
],
'notifications' => [
'disabled' => [
'title' => 'E-posta doğrulama kodları devre dışı bırakıldı',
],
],
];

View File

@@ -0,0 +1,38 @@
<?php
return [
'label' => 'Kur',
'modal' => [
'heading' => 'E-posta doğrulama kodlarını kur',
'description' => 'Her giriş yaptığınızda veya hassas işlemler gerçekleştirdiğinizde size e-posta ile gönderdiğimiz 6 haneli kodu girmeniz gerekecek. Kurulumu tamamlamak için e-postanızı kontrol edin ve 6 haneli kodu girin.',
'form' => [
'code' => [
'label' => 'Size e-posta ile gönderdiğimiz 6 haneli kodu girin',
'validation_attribute' => 'kod',
'actions' => [
'resend' => [
'label' => 'E-posta ile yeni kod gönder',
'notifications' => [
'resent' => [
'title' => 'Size e-posta ile yeni bir kod gönderdik',
],
],
],
],
'messages' => [
'invalid' => 'Girdiğiniz kod geçersiz.',
],
],
],
'actions' => [
'submit' => [
'label' => 'E-posta doğrulama kodlarını etkinleştir',
],
],
],
'notifications' => [
'enabled' => [
'title' => 'E-posta doğrulama kodları etkinleştirildi',
],
],
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'subject' => 'Giriş kodunuz',
'lines' => [
0 => 'Giriş kodunuz: :code',
1 => 'Bu kod 1 dakika içinde geçersiz olacak.|Bu kod :minutes dakika içinde geçersiz olacak.',
],
];

View File

@@ -0,0 +1,34 @@
<?php
return [
'management_schema' => [
'actions' => [
'label' => 'E-posta doğrulama kodları',
'below_content' => 'Giriş sırasında kimliğinizi doğrulamak için e-posta adresinize geçici bir kod alın.',
'messages' => [
'enabled' => 'Etkin',
'disabled' => 'Devre dışı',
],
],
],
'login_form' => [
'label' => 'E-postanıza kod gönder',
'code' => [
'label' => 'Size e-posta ile gönderdiğimiz 6 haneli kodu girin',
'validation_attribute' => 'kod',
'actions' => [
'resend' => [
'label' => 'E-posta ile yeni kod gönder',
'notifications' => [
'resent' => [
'title' => 'Size e-posta ile yeni bir kod gönderdik',
],
],
],
],
'messages' => [
'invalid' => 'Girdiğiniz kod geçersiz.',
],
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'title' => 'İki faktörlü kimlik doğrulamayı (2FA) kur',
'heading' => 'İki faktörlü kimlik doğrulamayı kur',
'subheading' => '2FA, giriş yaparken ikinci bir doğrulama formu gerektirerek hesabınıza ek bir güvenlik katmanı ekler.',
'actions' => [
'continue' => [
'label' => 'Devam et',
],
],
];

View File

@@ -0,0 +1,18 @@
<?php
return [
'actions' => [
0 => 'Tüm kodları',
'copy' => [
'label' => 'kopyalamak',
],
1 => 'or',
'download' => [
'label' => 'indirmek',
],
2 => 'için tıklayın.',
],
'messages' => [
'copied' => 'Göçürildi',
],
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'subject' => 'E-posta adresiniz güncelleniyor',
'lines' => [
0 => 'E-posta adresinizin güncellenmesi isteğini aldık. Bu güncelleme işlemi şifreniz kullanılarak doğrulanmıştır.',
1 => 'Doğrulanınca hesabınızın E-posta adresi belirtilen adres olarak güncellenecektir: :email.',
2 => 'Aşağıdaki butonu kullanarak E-posta adresi doğrulanmadan bu güncelleme isteğini iptal edebilirsiniz.',
3 => 'Eğer bu isteği siz yapmadıysanız lütfen bizimle iletişime geçin.',
],
'action' => 'Güncelleme İsteğini İptal Et',
];

View File

@@ -0,0 +1,48 @@
<?php
return [
'label' => 'Profil',
'form' => [
'email' => [
'label' => 'E-poçta salgysy',
],
'name' => [
'label' => 'Ad',
],
'password' => [
'label' => 'Yeni şifre',
'validation_attribute' => 'şifre',
],
'password_confirmation' => [
'label' => 'Yeni şifreyi onayla',
'validation_attribute' => 'şifre onayı',
],
'current_password' => [
'label' => 'Güncel şifre',
'below_content' => 'Güvenliğiniz için lütfen güncel şifrenizi girin.',
'validation_attribute' => 'güncel şifre',
],
'actions' => [
'save' => [
'label' => 'Değişiklikleri Kaydet',
],
],
],
'multi_factor_authentication' => [
'label' => 'İki Faktörlü Doğrulama (2FA)',
],
'notifications' => [
'email_change_verification_sent' => [
'title' => 'E-posta adresi güncelleme isteği gönderildi',
'body' => 'E-posta adresi güncelleme isteği :email adresine gönderildi. Lütfen güncellemeyi tamamlamak için E-posta adresinizi doğrulayın.',
],
'saved' => [
'title' => 'Kaydedildi',
],
],
'actions' => [
'cancel' => [
'label' => 'Ýatyr',
],
],
];

View File

@@ -0,0 +1,24 @@
<?php
return [
'title' => 'E-posta adresinizi doğrulayın',
'heading' => 'E-posta adresinizi doğrulayın',
'actions' => [
'resend_notification' => [
'label' => 'Yeniden Gönder',
],
],
'messages' => [
'notification_not_received' => 'Gönderdiğimiz e-postayı almadınız mı?',
'notification_sent' => ':email adresine, e-posta adresinizi nasıl doğrulayacağınıza ilişkin talimatları içeren bir e-posta gönderdik.',
],
'notifications' => [
'notification_resent' => [
'title' => 'E-posta yeniden gönderildi.',
],
'notification_resend_throttled' => [
'title' => 'Çok fazla yeniden gönderme denemesi',
'body' => ':seconds sekuntdan soň gaýtadan synanyşyň.',
],
],
];

View File

@@ -0,0 +1,85 @@
<?php
return [
'title' => 'Giriş',
'heading' => 'Girmek',
'actions' => [
'register' => [
'before' => 'ýa-da',
'label' => 'hasap açmak',
],
'request_password_reset' => [
'label' => 'Paroly ýatdan çykardyňyzmy?',
],
],
'form' => [
'email' => [
'label' => 'E-poçta salgysy',
],
'password' => [
'label' => 'Parol',
],
'remember' => [
'label' => 'Meni ýatda sakla',
],
'actions' => [
'authenticate' => [
'label' => 'Girmek',
],
],
],
'multi_factor' => [
'heading' => 'Şahsyýetiňizi tassyklaň',
'subheading' => 'Girmegi dowam etdirmek üçin şahsyýetiňizi tassyklaň.',
'form' => [
'provider' => [
'label' => 'Nähili tassyklamak isleýärsiňiz?',
],
'actions' => [
'authenticate' => [
'label' => 'Girişi tassykla',
],
],
],
],
'messages' => [
'failed' => 'Bu maglumatlar biziň ýazgylarymyza gabat gelenok.',
],
'notifications' => [
'throttled' => [
'title' => 'Giriş synanyşyklary köp',
'body' => ':seconds sekuntdan soň gaýtadan synanyşyň.',
],
],
];

View File

@@ -0,0 +1,30 @@
<?php
return [
'title' => 'Şifrenizi sıfırlayın',
'heading' => 'Şifrenizi mi unuttunuz?',
'actions' => [
'login' => [
'label' => 'girişe geri dön',
],
],
'form' => [
'email' => [
'label' => 'E-poçta salgysy',
],
'actions' => [
'request' => [
'label' => 'E-posta gönder',
],
],
],
'notifications' => [
'sent' => [
'body' => 'Eğer hesabınız yoksa bir e-posta almayacaksınız.',
],
'throttled' => [
'title' => 'Çok fazla istek',
'body' => ':seconds sekuntdan soň gaýtadan synanyşyň.',
],
],
];

View File

@@ -0,0 +1,29 @@
<?php
return [
'title' => 'Şifrenizi sıfırlayın',
'heading' => 'Şifrenizi sıfırlayın',
'form' => [
'email' => [
'label' => 'E-poçta salgysy',
],
'password' => [
'label' => 'Parol',
'validation_attribute' => 'password',
],
'password_confirmation' => [
'label' => 'Şifreyi onayla',
],
'actions' => [
'reset' => [
'label' => 'Şifreyi sıfırla',
],
],
],
'notifications' => [
'throttled' => [
'title' => 'Çok fazla sıfırlama denemesi',
'body' => ':seconds sekuntdan soň gaýtadan synanyşyň.',
],
],
];

View File

@@ -0,0 +1,38 @@
<?php
return [
'title' => 'Kayıt Ol',
'heading' => 'Üye Ol',
'actions' => [
'login' => [
'before' => 'ýa-da',
'label' => 'hesabınıza giriş yapın',
],
],
'form' => [
'email' => [
'label' => 'E-poçta salgysy',
],
'name' => [
'label' => 'Ad',
],
'password' => [
'label' => 'Parol',
'validation_attribute' => 'password',
],
'password_confirmation' => [
'label' => 'Şifreyi onayla',
],
'actions' => [
'register' => [
'label' => 'Üye ol',
],
],
],
'notifications' => [
'throttled' => [
'title' => 'Çok fazla kayıt denemesi',
'body' => ':seconds sekuntdan soň gaýtadan synanyşyň.',
],
],
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'title' => 'Sayfa yüklenirken hata oluştu',
'body' => 'Sayfa yüklenirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.',
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'field' => [
'label' => 'Genel arama',
'placeholder' => 'Gözleg',
],
'no_results_message' => 'Sonuç bulunamadı.',
];

View File

@@ -0,0 +1,72 @@
<?php
return [
'direction' => 'ltr',
'actions' => [
'billing' => [
'label' => 'Abunalygy dolandyr',
],
'logout' => [
'label' => 'Çykmak',
],
'open_database_notifications' => [
'label' => 'Habarnamalar',
],
'open_user_menu' => [
'label' => 'Ulanyjy menýusy',
],
'sidebar' => [
'collapse' => [
'label' => 'Gapdal paneli ýygnamak',
],
'expand' => [
'label' => 'Gapdal paneli giňeltmek',
],
],
'theme_switcher' => [
'dark' => [
'label' => 'Gara tema açmak',
],
'light' => [
'label' => 'Açyk tema açmak',
],
'system' => [
'label' => 'Ulgam temasyny açmak',
],
],
],
'avatar' => [
'alt' => ':name awatary',
],
'logo' => [
'alt' => ':name logotipi',
],
'tenant_menu' => [
'search_field' => [
'label' => 'Kirdeçi gözlegi',
'placeholder' => 'Gözleg',
],
],
];

View File

@@ -0,0 +1,33 @@
<?php
return [
'title' => 'Dolandyryş paneli',
'actions' => [
'filter' => [
'label' => 'Süzgüç',
'modal' => [
'heading' => 'Süzgüç',
'actions' => [
'apply' => [
'label' => 'Ulan',
],
],
],
],
],
];

View File

@@ -0,0 +1,16 @@
<?php
return [
'form' => [
'actions' => [
'save' => [
'label' => 'Değişiklikleri kaydet',
],
],
],
'notifications' => [
'saved' => [
'title' => 'Kaydedildi',
],
],
];

View File

@@ -0,0 +1,24 @@
<?php
return [
'title' => ':label oluştur',
'breadcrumb' => 'Döret',
'form' => [
'actions' => [
'cancel' => [
'label' => 'Ýatyr',
],
'create' => [
'label' => 'Döret',
],
'create_another' => [
'label' => 'Oluştur & yeni oluştur',
],
],
],
'notifications' => [
'created' => [
'title' => 'Oluşturuldu',
],
],
];

View File

@@ -0,0 +1,27 @@
<?php
return [
'title' => ':label üýtget',
'breadcrumb' => 'Üýtget',
'navigation_label' => 'Üýtget',
'form' => [
'actions' => [
'cancel' => [
'label' => 'Ýatyr',
],
'save' => [
'label' => 'Değişiklikleri kaydet',
],
],
],
'content' => [
'tab' => [
'label' => 'Üýtget',
],
],
'notifications' => [
'saved' => [
'title' => 'Kaydedildi',
],
],
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'breadcrumb' => 'Sanaw',
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'title' => ':label :relationship yönet',
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'title' => ':label gör',
'breadcrumb' => 'Gör',
'navigation_label' => 'Gör',
'content' => [
'tab' => [
'label' => 'Gör',
],
],
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'body' => 'Kayıt edilmemiş değişiklikleriniz mevcut. Bu sayfayı terk etmek istediğinize emin misiniz?',
];

View File

@@ -0,0 +1,10 @@
<?php
return [
'actions' => [
'logout' => [
'label' => 'Çykmak',
],
],
'welcome' => 'Hoş geldin',
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'actions' => [
'open_documentation' => [
'label' => 'Dokümantasyon',
],
'open_github' => [
'label' => 'GitHub',
],
],
];

View File

@@ -0,0 +1,333 @@
<?php
return [
'label' => 'Sorgu oluşturucu',
'form' => [
'operator' => [
'label' => 'Operatör',
],
'or_groups' => [
'label' => 'Gruplar',
'block' => [
'label' => 'Veya (OR)',
'or' => 'VEYA',
],
],
'rules' => [
'label' => 'Kurallar',
'item' => [
'and' => 'VE',
],
],
],
'no_rules' => '(Kural yok)',
'item_separators' => [
'and' => 'VE',
'or' => 'VEYA',
],
'operators' => [
'is_filled' => [
'label' => [
'direct' => 'Dolu',
'inverse' => 'Boş',
],
'summary' => [
'direct' => ':attribute dolu',
'inverse' => ':attribute boş',
],
],
'boolean' => [
'is_true' => [
'label' => [
'direct' => 'Doğru',
'inverse' => 'Yanlış',
],
'summary' => [
'direct' => ':attribute doğru',
'inverse' => ':attribute yanlış',
],
],
],
'date' => [
'is_after' => [
'label' => [
'direct' => 'Sonra',
'inverse' => 'Sonra değil',
],
'summary' => [
'direct' => ':attribute :date tarihinden sonra',
'inverse' => ':attribute :date tarihinden sonra değil',
],
],
'is_before' => [
'label' => [
'direct' => 'Önce',
'inverse' => 'Önce değil',
],
'summary' => [
'direct' => ':attribute :date tarihinden önce',
'inverse' => ':attribute :date tarihinden önce değil',
],
],
'is_date' => [
'label' => [
'direct' => 'Tarihtir',
'inverse' => 'Tarih değildir',
],
'summary' => [
'direct' => ':attribute :date',
'inverse' => ':attribute :date değil',
],
],
'is_month' => [
'label' => [
'direct' => 'Aydır',
'inverse' => 'Ay değildir',
],
'summary' => [
'direct' => ':attribute :month',
'inverse' => ':attribute :month değil',
],
],
'is_year' => [
'label' => [
'direct' => 'Yıldır',
'inverse' => 'Yıl değildir',
],
'summary' => [
'direct' => ':attribute :year',
'inverse' => ':attribute :year değil',
],
],
'form' => [
'date' => [
'label' => 'Tarih',
],
'month' => [
'label' => 'Ay',
],
'year' => [
'label' => 'Yıl',
],
],
],
'number' => [
'equals' => [
'label' => [
'direct' => 'Eşittir',
'inverse' => 'Eşit değildir',
],
'summary' => [
'direct' => ':attribute :number\'a eşittir',
'inverse' => ':attribute :number\'a eşit değildir',
],
],
'is_max' => [
'label' => [
'direct' => 'Maksimum',
'inverse' => 'Büyüktür',
],
'summary' => [
'direct' => ':attribute maksimum :number',
'inverse' => ':attribute :number\'dan büyük',
],
],
'is_min' => [
'label' => [
'direct' => 'Minimum',
'inverse' => 'Küçüktür',
],
'summary' => [
'direct' => ':attribute minimum :number',
'inverse' => ':attribute :number\'dan küçük',
],
],
'aggregates' => [
'average' => [
'label' => 'Ortalama',
'summary' => 'Ortalama :attribute',
],
'max' => [
'label' => 'Maksimum',
'summary' => 'Maksimum :attribute',
],
'min' => [
'label' => 'Min',
'summary' => 'Min :attribute',
],
'sum' => [
'label' => 'Toplam',
'summary' => ':attribute toplamı',
],
],
'form' => [
'aggregate' => [
'label' => 'Toplam',
],
'number' => [
'label' => 'Sayı',
],
],
],
'relationship' => [
'equals' => [
'label' => [
'direct' => 'Sahip',
'inverse' => 'Sahip değil',
],
'summary' => [
'direct' => ':count :relationship mevcut',
'inverse' => ':count :relationship mevcut değil',
],
],
'has_max' => [
'label' => [
'direct' => 'En fazla',
'inverse' => 'Has köp',
],
'summary' => [
'direct' => 'En fazla :count :relationship',
'inverse' => ':count adetten fazla :relationship',
],
],
'has_min' => [
'label' => [
'direct' => 'En az',
'inverse' => 'Has az',
],
'summary' => [
'direct' => 'En az :count :relationship',
'inverse' => ':count adetten az :relationship',
],
],
'is_empty' => [
'label' => [
'direct' => 'Boş',
'inverse' => 'Boş değil',
],
'summary' => [
'direct' => ':relationship boş',
'inverse' => ':relationship boş değil',
],
],
'is_related_to' => [
'label' => [
'single' => [
'direct' => 'Eşittir',
'inverse' => 'Eşit değildir',
],
'multiple' => [
'direct' => 'İçerir',
'inverse' => 'İçermez',
],
],
'summary' => [
'single' => [
'direct' => ':relationship :values',
'inverse' => ':relationship :values değil',
],
'multiple' => [
'direct' => ':relationship :values içerir',
'inverse' => ':relationship :values içermez',
],
'values_glue' => [
0 => ', ',
'final' => ' veya ',
],
],
'form' => [
'value' => [
'label' => 'Değer',
],
'values' => [
'label' => 'Değerler',
],
],
],
'form' => [
'count' => [
'label' => 'Sayı',
],
],
],
'select' => [
'is' => [
'label' => [
'direct' => 'Eşittir',
'inverse' => 'Eşit değildir',
],
'summary' => [
'direct' => ':attribute :values',
'inverse' => ':attribute :values değil',
'values_glue' => [
0 => ', ',
'final' => ' veya ',
],
],
'form' => [
'value' => [
'label' => 'Değer',
],
'values' => [
'label' => 'Değerler',
],
],
],
],
'text' => [
'contains' => [
'label' => [
'direct' => 'İçerir',
'inverse' => 'İçermez',
],
'summary' => [
'direct' => ':attribute :text içerir',
'inverse' => ':attribute :text içermez',
],
],
'ends_with' => [
'label' => [
'direct' => 'Şununla biter',
'inverse' => 'Şununla bitmez',
],
'summary' => [
'direct' => ':attribute :text ile biter',
'inverse' => ':attribute :text ile bitmez',
],
],
'equals' => [
'label' => [
'direct' => 'Eşittir',
'inverse' => 'Eşit değildir',
],
'summary' => [
'direct' => ':attribute :text\'e eşittir',
'inverse' => ':attribute :text\'e eşit değildir',
],
],
'starts_with' => [
'label' => [
'direct' => 'Şununla başlar',
'inverse' => 'Şununla başlamaz',
],
'summary' => [
'direct' => ':attribute :text ile başlar',
'inverse' => ':attribute :text ile başlamaz',
],
],
'form' => [
'text' => [
'label' => 'Metin',
],
],
],
],
'actions' => [
'add_rule' => [
'label' => 'Kural ekle',
],
'add_rule_group' => [
'label' => 'Kural grubu ekle',
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'wizard' => [
'actions' => [
'previous_step' => [
'label' => 'Yza',
],
'next_step' => [
'label' => 'İleri',
],
],
],
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'messages' => [
'uploading_file' => 'Faýl ýüklenýär...',
],
];

View File

@@ -0,0 +1,7 @@
<?php
return [
'messages' => [
'copied' => 'Göçürildi',
],
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'actions' => [
'close' => [
'label' => 'Ýap',
],
],
];

View File

@@ -0,0 +1,47 @@
<?php
return [
'label' => 'Sahypalar navigasiýasy',
'overview' => '{1} 1 netije görkezilýär|[2,*] :total netijeden :first:last görkezilýär',
'fields' => [
'records_per_page' => [
'label' => 'Sahypada',
'options' => [
'all' => 'Hemmesi',
],
],
],
'actions' => [
'first' => [
'label' => 'Ilkinji',
],
'go_to_page' => [
'label' => ':page sahypa geç',
],
'last' => [
'label' => 'Soňky',
],
'next' => [
'label' => 'Indiki',
],
'previous' => [
'label' => 'Öňki',
],
],
];

267
lang/vendor/filament-tables/tk/table.php vendored Normal file
View File

@@ -0,0 +1,267 @@
<?php
return [
'column_manager' => [
'heading' => 'Sütünler',
'actions' => [
'apply' => [
'label' => 'Sütünleri ulan',
],
'reset' => [
'label' => 'Täzeden',
],
],
],
'columns' => [
'actions' => [
'label' => 'Hereket|Hereketler',
],
'select' => [
'loading_message' => 'Ýüklenýär...',
'no_options_message' => 'Saýlaw ýok.',
'no_search_results_message' => 'Gözlege laýyk saýlaw tapylmady.',
'placeholder' => 'Saýlaw saýlaň',
'searching_message' => 'Gözlenýär...',
'search_prompt' => 'Gözlemek üçin ýazyň...',
],
'text' => [
'actions' => [
'collapse_list' => ':count az görkez',
'expand_list' => ':count köp görkez',
],
'more_list_items' => 'we ýene :count',
],
],
'fields' => [
'bulk_select_page' => [
'label' => 'Köp hereketler üçin ähli elementleri saýla/saýlawy aýyr.',
],
'bulk_select_record' => [
'label' => 'Köp hereketler üçin :key elementini saýla/saýlawy aýyr.',
],
'bulk_select_group' => [
'label' => 'Köp hereketler üçin :title toparyny saýla/saýlawy aýyr.',
],
'search' => [
'label' => 'Gözleg',
'placeholder' => 'Gözleg',
'indicator' => 'Gözleg',
],
],
'summary' => [
'heading' => 'Jemi',
'subheadings' => [
'all' => 'Ähli :label',
'group' => ':group jemi',
'page' => 'Bu sahypa',
],
'summarizers' => [
'average' => [
'label' => 'Ortaça',
],
'count' => [
'label' => 'Sany',
],
'sum' => [
'label' => 'Jemi',
],
],
],
'actions' => [
'disable_reordering' => [
'label' => 'Tertiplemegi tamamla',
],
'enable_reordering' => [
'label' => 'Ýazgylary tertiple',
],
'filter' => [
'label' => 'Süzgüç',
],
'group' => [
'label' => 'Toparla',
],
'open_bulk_actions' => [
'label' => 'Köp hereketler',
],
'column_manager' => [
'label' => 'Sütün dolandyryşy',
],
],
'empty' => [
'heading' => ':model ýok',
'description' => 'Başlamak üçin :model dörediň.',
],
'filters' => [
'actions' => [
'apply' => [
'label' => 'Süzgüçleri ulan',
],
'remove' => [
'label' => 'Süzgüçi aýyr',
],
'remove_all' => [
'label' => 'Ähli süzgüçleri aýyr',
'tooltip' => 'Ähli süzgüçleri aýyr',
],
'reset' => [
'label' => 'Täzeden',
],
],
'heading' => 'Süzgüçler',
'indicator' => 'Işjeň süzgüçler',
'multi_select' => [
'placeholder' => 'Hemmesi',
],
'select' => [
'placeholder' => 'Hemmesi',
'relationship' => [
'empty_option_label' => 'Ýok',
],
],
'trashed' => [
'label' => 'Pozulan ýazgylar',
'only_trashed' => 'Diňe pozulan ýazgylar',
'with_trashed' => 'Pozulanlar bilen',
'without_trashed' => 'Pozulmadyklar',
],
],
'grouping' => [
'fields' => [
'group' => [
'label' => 'Toparla',
],
'direction' => [
'label' => 'Topar ugry',
'options' => [
'asc' => 'Artýan',
'desc' => 'Pesýän',
],
],
],
],
'reorder_indicator' => 'Ýazgylary tertiplemek üçin süýşüriň.',
'selection_indicator' => [
'selected_count' => '1 ýazgy saýlandy|:count ýazgy saýlandy',
'actions' => [
'select_all' => [
'label' => 'Ähli :count saýla',
],
'deselect_all' => [
'label' => 'Saýlawy aýyr',
],
],
],
'sorting' => [
'fields' => [
'column' => [
'label' => 'Tertiple',
],
'direction' => [
'label' => 'Tertip ugry',
'options' => [
'asc' => 'Artýan',
'desc' => 'Pesýän',
],
],
],
],
'default_model_label' => 'ýazgy',
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Süzgüç',
],
],
];

View File

@@ -1,25 +1,3 @@
<x-filament-panels::page> <x-filament-panels::page>
@if ($activeCampaignId)
<div
wire:poll.2s="refreshCampaignProgress"
class="mb-6 rounded-xl border border-primary-200 bg-primary-50 p-4 dark:border-primary-800 dark:bg-primary-950"
>
<div class="mb-2 flex items-center justify-between gap-4">
<p class="text-sm font-medium text-primary-900 dark:text-primary-100">
Sending messages…
</p>
<p class="text-sm text-primary-700 dark:text-primary-300">
{{ $campaignProgressLabel }}
</p>
</div>
<div class="h-2 overflow-hidden rounded-full bg-primary-200 dark:bg-primary-900">
<div
class="h-full rounded-full bg-primary-600 transition-all duration-300 dark:bg-primary-400"
style="width: {{ $campaignProgressPercent }}%"
></div>
</div>
</div>
@endif
{{ $this->content }} {{ $this->content }}
</x-filament-panels::page> </x-filament-panels::page>

View File

@@ -78,9 +78,13 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="w-full max-w-sm mt-2xl flex flex-col gap-md"> <div class="w-full max-w-sm mt-2xl flex flex-col gap-md">
<button class="w-full bg-primary text-on-primary py-md rounded-full font-label-md text-label-md shadow-[0_4px_12px_rgba(0,105,72,0.3)] hover:opacity-90 active:scale-95 transition-all"> <a
Häzir ulan href="https://daragt.com"
</button> target="_blank"
class="w-full bg-primary text-on-primary py-md rounded-full font-label-md text-label-md shadow-[0_4px_12px_rgba(0,105,72,0.3)] hover:opacity-90 active:scale-95 transition-all text-center"
>
Daragt harytlary görmek
</a>
</div> </div>
<!-- Info Card --> <!-- Info Card -->

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\Feature;
use App\Jobs\SendSmsToCouponJob;
use App\Jobs\StartSmsCampaignJob;
use App\Models\Coupon;
use App\Models\User;
use App\Services\SmsBroadcastService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class StartSmsCampaignJobTest extends TestCase
{
use LazilyRefreshDatabase;
public function test_job_is_dispatched_with_expected_payload(): void
{
Queue::fake();
$admin = User::factory()->create();
$coupon = Coupon::factory()->create();
StartSmsCampaignJob::dispatch(
adminId: $admin->id,
message: 'Hello',
sendToAll: false,
selectedIds: [$coupon->id],
excludedIds: [],
);
Queue::assertPushed(StartSmsCampaignJob::class, function (StartSmsCampaignJob $job) use ($admin, $coupon): bool {
return $job->adminId === $admin->id
&& $job->message === 'Hello'
&& $job->sendToAll === false
&& $job->selectedIds === [$coupon->id]
&& $job->excludedIds === [];
});
}
public function test_handle_starts_campaign_and_chains_send_jobs(): void
{
Bus::fake();
$admin = User::factory()->create();
$coupons = Coupon::factory()->count(2)->create();
$job = new StartSmsCampaignJob(
adminId: $admin->id,
message: 'Test broadcast',
sendToAll: false,
selectedIds: $coupons->pluck('id')->all(),
);
$job->handle(app(SmsBroadcastService::class));
Bus::assertChained([
SendSmsToCouponJob::class,
SendSmsToCouponJob::class,
]);
}
}

View File

@@ -64,9 +64,9 @@ class SmsMessageAnalyzerTest extends TestCase
public static function summaryProvider(): array public static function summaryProvider(): array
{ {
return [ return [
'empty' => ['', '0 characters'], 'empty' => ['', '0 harp'],
'gsm' => ['Hello', '5 characters · 1 SMS'], 'gsm' => ['Hello', '5 harp · 1 SMS'],
'unicode' => ['你好', '2 characters · 1 SMS'], 'unicode' => ['你好', '2 harp · 1 SMS'],
]; ];
} }
} }