diff --git a/app/Enums/Gender.php b/app/Enums/Gender.php index 024f70b..8aaaa24 100644 --- a/app/Enums/Gender.php +++ b/app/Enums/Gender.php @@ -8,14 +8,14 @@ enum Gender: string { case Male = 'male'; case Female = 'female'; - case Other = 'other'; + // case Other = 'other'; public function label(): string { return match ($this) { self::Male => __('enums.gender.male'), self::Female => __('enums.gender.female'), - self::Other => __('enums.gender.other'), + // self::Other => __('enums.gender.other'), }; } @@ -24,7 +24,7 @@ enum Gender: string return match ($this) { self::Male => 'info', self::Female => 'pink', - self::Other => 'gray', + // self::Other => 'gray', }; } @@ -33,7 +33,7 @@ enum Gender: string return match ($this) { self::Male => 'heroicon-o-user', self::Female => 'heroicon-o-user', - self::Other => 'heroicon-o-users', + // self::Other => 'heroicon-o-users', }; } } diff --git a/app/Exports/EmployeesExport.php b/app/Exports/EmployeesExport.php index d2f74fa..5422e37 100644 --- a/app/Exports/EmployeesExport.php +++ b/app/Exports/EmployeesExport.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Exports; use App\Models\Employee; +use App\Support\Countries; use Illuminate\Database\Eloquent\Builder; use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\WithHeadings; @@ -39,7 +40,7 @@ class EmployeesExport implements FromQuery, WithHeadings, WithMapping return [ 'Employee Number', 'Full Name', - 'National ID', + 'Nationality', 'Gender', 'Birth Date', 'Phone', @@ -63,7 +64,7 @@ class EmployeesExport implements FromQuery, WithHeadings, WithMapping return [ $employee->employee_number, $employee->full_name, - $employee->national_id, + Countries::name($employee->national_id) ?? $employee->national_id, $employee->gender?->value, $employee->birth_date?->toDateString(), $employee->phone, diff --git a/app/Filament/Resources/Employees/Schemas/EmployeeForm.php b/app/Filament/Resources/Employees/Schemas/EmployeeForm.php index 230f38a..9eefe79 100644 --- a/app/Filament/Resources/Employees/Schemas/EmployeeForm.php +++ b/app/Filament/Resources/Employees/Schemas/EmployeeForm.php @@ -5,10 +5,11 @@ namespace App\Filament\Resources\Employees\Schemas; use App\Enums\EmploymentStatus; use App\Enums\Gender; use App\Filament\Support\HrForm; +use App\Support\Countries; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Schema; @@ -30,24 +31,32 @@ class EmployeeForm ->imageEditor(), TextInput::make('employee_number') ->label(__('hr.fields.employee_number')) - ->required() - ->maxLength(50) - ->unique(ignoreRecord: true), + ->disabled() + ->dehydrated(false) + ->visibleOn('edit'), TextInput::make('full_name') ->label(__('hr.fields.full_name')) ->required() ->maxLength(255), - TextInput::make('national_id') + Select::make('national_id') ->label(__('hr.fields.national_id')) - ->maxLength(50), + ->options(fn (): array => Countries::options()) + ->searchable() + ->native(false) + ->default(Countries::default()) + ->required(), + Select::make('gender') ->label(__('hr.fields.gender')) ->options(Gender::class) ->required() ->native(false), + DatePicker::make('birth_date') ->label(__('hr.fields.birth_date')) - ->required(), + ->required() + ->native(false), + TextInput::make('phone') ->label(__('hr.fields.phone')) ->tel() diff --git a/app/Filament/Resources/Employees/Schemas/EmployeeInfolist.php b/app/Filament/Resources/Employees/Schemas/EmployeeInfolist.php index 7860487..543e186 100644 --- a/app/Filament/Resources/Employees/Schemas/EmployeeInfolist.php +++ b/app/Filament/Resources/Employees/Schemas/EmployeeInfolist.php @@ -9,6 +9,7 @@ use App\Enums\Gender; use App\Models\Employee; use App\Models\Vacation; use App\Services\Employee\EmployeeStatisticsService; +use App\Support\Countries; use Filament\Infolists\Components\ImageEntry; use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Components\Section; @@ -42,6 +43,7 @@ class EmployeeInfolist ->label(__('hr.fields.full_name')), TextEntry::make('national_id') ->label(__('hr.fields.national_id')) + ->formatStateUsing(fn (?string $state): string => Countries::name($state) ?? $state ?? '—') ->placeholder('—'), TextEntry::make('gender') ->label(__('hr.fields.gender')) @@ -153,16 +155,16 @@ class EmployeeInfolist TextEntry::make('leave_balance_taken') ->label(__('hr.fields.days_taken')) ->state(fn (Employee $record): int => app(EmployeeStatisticsService::class)->summary($record)->vacationTaken) - ->suffix(' ' . __('hr.messages.days_suffix')), + ->suffix(' '.__('hr.messages.days_suffix')), TextEntry::make('leave_balance_remaining') ->label(__('hr.fields.days_remaining')) ->state(fn (Employee $record): int => app(EmployeeStatisticsService::class)->summary($record)->vacationRemaining) - ->suffix(' ' . __('hr.messages.days_suffix')) + ->suffix(' '.__('hr.messages.days_suffix')) ->color('success'), TextEntry::make('leave_balance_total') ->label(__('hr.fields.annual_allowance')) ->state(fn (): int => (int) config('hr.annual_leave_days')) - ->suffix(' ' . __('hr.messages.days_suffix')), + ->suffix(' '.__('hr.messages.days_suffix')), ]) ->columns(3), Section::make(__('hr.sections.upcoming_approved_leave')) diff --git a/app/Filament/Resources/Employees/Tables/EmployeesTable.php b/app/Filament/Resources/Employees/Tables/EmployeesTable.php index ea9282c..a7a57ed 100644 --- a/app/Filament/Resources/Employees/Tables/EmployeesTable.php +++ b/app/Filament/Resources/Employees/Tables/EmployeesTable.php @@ -4,9 +4,11 @@ namespace App\Filament\Resources\Employees\Tables; use App\Enums\EmploymentStatus; use App\Enums\Gender; +use App\Exports\EmployeesExport; +use App\Filament\Support\ExportExcelAction; use App\Models\Department; use App\Models\Employee; -use App\Filament\Support\ExportExcelAction; +use App\Support\Countries; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; @@ -37,6 +39,7 @@ class EmployeesTable ->sortable(), TextColumn::make('national_id') ->label(__('hr.fields.national_id')) + ->formatStateUsing(fn (?string $state): ?string => Countries::name($state) ?? $state) ->searchable() ->toggleable(), TextColumn::make('gender') @@ -126,7 +129,7 @@ class EmployeesTable ]) ->toolbarActions([ BulkActionGroup::make([ - ExportExcelAction::bulk('exportSelected', \App\Exports\EmployeesExport::class, 'employees.xlsx'), + ExportExcelAction::bulk('exportSelected', EmployeesExport::class, 'employees.xlsx'), BulkAction::make('changeDepartment') ->label(__('hr.actions.change_department')) ->icon('heroicon-o-building-office-2') diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..d93bc5a --- /dev/null +++ b/app/Http/Middleware/SetLocale.php @@ -0,0 +1,30 @@ +user()) { + return $next($request); + } + + $locale = $request->user()->locale; + + if (isset($locale)) { + app()->setLocale($locale); + } + + return $next($request); + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php index 08d6183..75ff2dc 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -48,6 +48,38 @@ class Employee extends Model ]; } + protected static function booted(): void + { + static::saving(function (Employee $employee): void { + if (blank($employee->employee_number)) { + $employee->employee_number = static::generateUniqueEmployeeNumber(); + } + }); + } + + public static function generateUniqueEmployeeNumber(): string + { + $prefix = (string) config('hr.employee_number.prefix', 'EMP-'); + $padding = (int) config('hr.employee_number.padding', 5); + + $latestNumber = static::withTrashed() + ->where('employee_number', 'like', $prefix.'%') + ->pluck('employee_number') + ->map(function (string $number) use ($prefix): ?int { + if (! preg_match('/^'.preg_quote($prefix, '/').'(\d+)$/', $number, $matches)) { + return null; + } + + return (int) $matches[1]; + }) + ->filter() + ->max(); + + $next = ($latestNumber ?? 0) + 1; + + return $prefix.str_pad((string) $next, $padding, '0', STR_PAD_LEFT); + } + public function department(): BelongsTo { return $this->belongsTo(Department::class); diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 590165a..b5b05cb 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -4,13 +4,15 @@ declare(strict_types=1); namespace App\Providers\Filament; -use BezhanSalleh\FilamentShield\FilamentShieldPlugin; -use App\Enums\NavigationGroup; +use App\Enums\NavigationGroup as NavigationGroupEnum; use App\Filament\Pages\Dashboard; +use BezhanSalleh\FilamentShield\FilamentShieldPlugin; +use Filament\FontProviders\GoogleFontProvider; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; +use Filament\Navigation\NavigationGroup as FilamentNavigationGroup; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; @@ -20,7 +22,6 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; -use Filament\FontProviders\GoogleFontProvider; class AdminPanelProvider extends PanelProvider { @@ -39,7 +40,14 @@ class AdminPanelProvider extends PanelProvider 'gray' => Color::Slate, ]) ->font('Inter', provider: GoogleFontProvider::class) - ->navigationGroups(NavigationGroup::class) + ->navigationGroups( + collect(NavigationGroupEnum::cases()) + ->mapWithKeys(fn (NavigationGroupEnum $case): array => [ + $case->name => FilamentNavigationGroup::make() + ->label(fn (): ?string => $case->getLabel()), + ]) + ->all(), + ) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') ->pages([ @@ -49,7 +57,7 @@ class AdminPanelProvider extends PanelProvider ->widgets([]) ->plugins([ FilamentShieldPlugin::make() - ->navigationGroup(NavigationGroup::System), + ->navigationGroup(NavigationGroupEnum::System), ]) ->middleware([ EncryptCookies::class, diff --git a/app/Services/Import/EmployeeImportTransformer.php b/app/Services/Import/EmployeeImportTransformer.php index a2cf999..ee62fb5 100644 --- a/app/Services/Import/EmployeeImportTransformer.php +++ b/app/Services/Import/EmployeeImportTransformer.php @@ -11,6 +11,7 @@ use App\Models\Department; use App\Models\Employee; use App\Models\Position; use App\Models\Shift; +use App\Support\Countries; use Carbon\Carbon; use Illuminate\Support\Str; @@ -22,9 +23,9 @@ class EmployeeImportTransformer public function toAttributes(EmployeeRowDto $dto): array { return [ - 'employee_number' => $dto->employeeNumber, + 'employee_number' => $dto->employeeNumber !== '' ? $dto->employeeNumber : null, 'full_name' => $dto->fullName, - 'national_id' => $dto->nationalId, + 'national_id' => $this->resolveNationality($dto->nationalId), 'gender' => $this->resolveGender($dto->gender), 'birth_date' => $this->parseDate($dto->birthDate), 'phone' => $dto->phone ?? config('hr.default_phone'), @@ -45,10 +46,6 @@ class EmployeeImportTransformer { $errors = []; - if ($dto->employeeNumber === '') { - $errors[] = 'Employee number is required.'; - } - if ($dto->fullName === '') { $errors[] = 'Full name is required.'; } @@ -65,22 +62,42 @@ class EmployeeImportTransformer $errors[] = "Shift \"{$dto->shift}\" was not found."; } + if ($dto->nationalId && $this->resolveNationality($dto->nationalId) === null) { + $errors[] = "Nationality \"{$dto->nationalId}\" was not found."; + } + return $errors; } public function isDuplicate(EmployeeRowDto $dto): bool { - return Employee::query() - ->where(function ($query) use ($dto): void { - $query->where('employee_number', $dto->employeeNumber); + $hasEmployeeNumber = $dto->employeeNumber !== ''; + $nationality = $this->resolveNationality($dto->nationalId); - if ($dto->nationalId) { - $query->orWhere('national_id', $dto->nationalId); + if (! $hasEmployeeNumber && $nationality === null) { + return false; + } + + return Employee::query() + ->where(function ($query) use ($dto, $hasEmployeeNumber, $nationality): void { + if ($hasEmployeeNumber) { + $query->where('employee_number', $dto->employeeNumber); + } + + if ($nationality !== null) { + $hasEmployeeNumber + ? $query->orWhere('national_id', $nationality) + : $query->where('national_id', $nationality); } }) ->exists(); } + private function resolveNationality(?string $value): ?string + { + return Countries::resolve($value); + } + private function resolveGender(?string $value): ?Gender { if ($value === null) { diff --git a/app/Support/Countries.php b/app/Support/Countries.php new file mode 100644 index 0000000..4bf5785 --- /dev/null +++ b/app/Support/Countries.php @@ -0,0 +1,124 @@ + + */ + public static function codes(): array + { + /** @var array{codes: array} $config */ + $config = config('countries'); + + return $config['codes']; + } + + /** + * @return array + */ + public static function options(?string $locale = null): array + { + $locale ??= app()->getLocale(); + $options = []; + + foreach (self::codes() as $code) { + $name = self::name($code, $locale); + + if ($name !== null) { + $options[$code] = $name; + } + } + + uasort($options, fn (string $a, string $b): int => strcasecmp($a, $b)); + + return $options; + } + + public static function name(?string $code, ?string $locale = null): ?string + { + if ($code === null || $code === '') { + return null; + } + + $code = strtoupper($code); + $locale ??= app()->getLocale(); + + $name = self::translation($code, $locale); + + if ($name === null && $locale !== 'en') { + $name = self::translation($code, 'en'); + } + + return $name; + } + + public static function isValid(?string $code): bool + { + if ($code === null || $code === '') { + return false; + } + + return in_array(strtoupper($code), self::codes(), true); + } + + public static function resolve(?string $value, ?string $locale = null): ?string + { + if ($value === null || trim($value) === '') { + return null; + } + + $value = trim($value); + $upper = strtoupper($value); + + if (self::isValid($upper)) { + return $upper; + } + + $locale ??= app()->getLocale(); + + foreach (self::codes() as $code) { + $name = self::name($code, $locale); + + if ($name !== null && strcasecmp($name, $value) === 0) { + return $code; + } + } + + if ($locale !== 'en') { + foreach (self::codes() as $code) { + $name = self::name($code, 'en'); + + if ($name !== null && strcasecmp($name, $value) === 0) { + return $code; + } + } + } + + return null; + } + + private static function translation(string $code, string $locale): ?string + { + $path = lang_path("{$locale}/countries.php"); + + if (! is_file($path)) { + return null; + } + + /** @var array $names */ + $names = require $path; + + return $names[$code] ?? null; + } + + public static function default(): string + { + return self::DEFAULT; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7a2848f..be28e62 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Http\Request; +use App\Http\Middleware\SetLocale; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,7 +13,7 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + // $middleware->web(SetLocale::class); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/config/countries.php b/config/countries.php new file mode 100644 index 0000000..b467ebf --- /dev/null +++ b/config/countries.php @@ -0,0 +1,204 @@ + [ + 0 => 'TM', + 1 => 'RU', + 2 => 'KZ', + 3 => 'UZ', + 4 => 'TR', + 5 => 'IR', + 6 => 'AF', + 7 => 'AZ', + 8 => 'KG', + 9 => 'TJ', + 10 => 'UA', + 11 => 'CN', + 12 => 'DE', + 13 => 'US', + 14 => 'GB', + 15 => 'AD', + 16 => 'AE', + 17 => 'AG', + 18 => 'AL', + 19 => 'AM', + 20 => 'AO', + 21 => 'AR', + 22 => 'AT', + 23 => 'AU', + 24 => 'BA', + 25 => 'BB', + 26 => 'BD', + 27 => 'BE', + 28 => 'BF', + 29 => 'BG', + 30 => 'BH', + 31 => 'BI', + 32 => 'BJ', + 33 => 'BN', + 34 => 'BO', + 35 => 'BR', + 36 => 'BS', + 37 => 'BT', + 38 => 'BW', + 39 => 'BY', + 40 => 'BZ', + 41 => 'CA', + 42 => 'CD', + 43 => 'CF', + 44 => 'CG', + 45 => 'CH', + 46 => 'CI', + 47 => 'CL', + 48 => 'CM', + 49 => 'CO', + 50 => 'CR', + 51 => 'CU', + 52 => 'CV', + 53 => 'CY', + 54 => 'CZ', + 55 => 'DJ', + 56 => 'DK', + 57 => 'DM', + 58 => 'DO', + 59 => 'DZ', + 60 => 'EC', + 61 => 'EE', + 62 => 'EG', + 63 => 'ER', + 64 => 'ES', + 65 => 'ET', + 66 => 'FI', + 67 => 'FJ', + 68 => 'FM', + 69 => 'FR', + 70 => 'GA', + 71 => 'GD', + 72 => 'GE', + 73 => 'GH', + 74 => 'GM', + 75 => 'GN', + 76 => 'GQ', + 77 => 'GR', + 78 => 'GT', + 79 => 'GW', + 80 => 'GY', + 81 => 'HN', + 82 => 'HR', + 83 => 'HT', + 84 => 'HU', + 85 => 'ID', + 86 => 'IE', + 87 => 'IL', + 88 => 'IN', + 89 => 'IQ', + 90 => 'IS', + 91 => 'IT', + 92 => 'JM', + 93 => 'JO', + 94 => 'JP', + 95 => 'KE', + 96 => 'KH', + 97 => 'KI', + 98 => 'KM', + 99 => 'KN', + 100 => 'KP', + 101 => 'KR', + 102 => 'KW', + 103 => 'LA', + 104 => 'LB', + 105 => 'LC', + 106 => 'LI', + 107 => 'LK', + 108 => 'LR', + 109 => 'LS', + 110 => 'LT', + 111 => 'LU', + 112 => 'LV', + 113 => 'LY', + 114 => 'MA', + 115 => 'MC', + 116 => 'MD', + 117 => 'ME', + 118 => 'MG', + 119 => 'MH', + 120 => 'MK', + 121 => 'ML', + 122 => 'MM', + 123 => 'MN', + 124 => 'MR', + 125 => 'MT', + 126 => 'MU', + 127 => 'MV', + 128 => 'MW', + 129 => 'MX', + 130 => 'MY', + 131 => 'MZ', + 132 => 'NA', + 133 => 'NE', + 134 => 'NG', + 135 => 'NI', + 136 => 'NL', + 137 => 'NO', + 138 => 'NP', + 139 => 'NR', + 140 => 'NZ', + 141 => 'OM', + 142 => 'PA', + 143 => 'PE', + 144 => 'PG', + 145 => 'PH', + 146 => 'PK', + 147 => 'PL', + 148 => 'PS', + 149 => 'PT', + 150 => 'PW', + 151 => 'PY', + 152 => 'QA', + 153 => 'RO', + 154 => 'RS', + 155 => 'RW', + 156 => 'SA', + 157 => 'SB', + 158 => 'SC', + 159 => 'SD', + 160 => 'SE', + 161 => 'SG', + 162 => 'SI', + 163 => 'SK', + 164 => 'SL', + 165 => 'SM', + 166 => 'SN', + 167 => 'SO', + 168 => 'SR', + 169 => 'SS', + 170 => 'ST', + 171 => 'SV', + 172 => 'SY', + 173 => 'SZ', + 174 => 'TD', + 175 => 'TG', + 176 => 'TH', + 177 => 'TL', + 178 => 'TN', + 179 => 'TO', + 180 => 'TT', + 181 => 'TV', + 182 => 'TW', + 183 => 'TZ', + 184 => 'UG', + 185 => 'UY', + 186 => 'VA', + 187 => 'VC', + 188 => 'VE', + 189 => 'VN', + 190 => 'VU', + 191 => 'WS', + 192 => 'YE', + 193 => 'ZA', + 194 => 'ZM', + 195 => 'ZW', + ], +]; diff --git a/config/hr.php b/config/hr.php index d7f92b4..bf38d6e 100644 --- a/config/hr.php +++ b/config/hr.php @@ -12,6 +12,11 @@ return [ 'default_phone' => '+993 61 92 92 48', + 'employee_number' => [ + 'prefix' => 'EMP-', + 'padding' => 5, + ], + 'file_uploads' => [ 'disk' => 'local', 'directory' => 'hr', diff --git a/database/factories/EmployeeFactory.php b/database/factories/EmployeeFactory.php index 7d6457a..20849aa 100644 --- a/database/factories/EmployeeFactory.php +++ b/database/factories/EmployeeFactory.php @@ -10,6 +10,7 @@ use App\Models\Department; use App\Models\Employee; use App\Models\Position; use App\Models\Shift; +use App\Support\Countries; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -30,9 +31,9 @@ class EmployeeFactory extends Factory $gender = fake()->randomElement(Gender::cases()); return [ - 'employee_number' => 'EMP-'.fake()->unique()->numerify('#####'), + 'employee_number' => '', 'full_name' => fake()->name($gender === Gender::Female ? 'female' : 'male'), - 'national_id' => fake()->optional(0.8)->numerify('##########'), + 'national_id' => fake()->optional(0.8)->randomElement(Countries::codes()), 'gender' => $gender, 'birth_date' => $birthDate, 'phone' => '+993 61 92 92 48', diff --git a/lang/en/countries.php b/lang/en/countries.php new file mode 100644 index 0000000..15bccaf --- /dev/null +++ b/lang/en/countries.php @@ -0,0 +1,202 @@ + 'Afghanistan', + 'AL' => 'Albania', + 'DZ' => 'Algeria', + 'AD' => 'Andorra', + 'AO' => 'Angola', + 'AG' => 'Antigua and Barbuda', + 'AR' => 'Argentina', + 'AM' => 'Armenia', + 'AU' => 'Australia', + 'AT' => 'Austria', + 'AZ' => 'Azerbaijan', + 'BS' => 'Bahamas', + 'BH' => 'Bahrain', + 'BD' => 'Bangladesh', + 'BB' => 'Barbados', + 'BY' => 'Belarus', + 'BE' => 'Belgium', + 'BZ' => 'Belize', + 'BJ' => 'Benin', + 'BT' => 'Bhutan', + 'BO' => 'Bolivia', + 'BA' => 'Bosnia and Herzegovina', + 'BW' => 'Botswana', + 'BR' => 'Brazil', + 'BN' => 'Brunei', + 'BG' => 'Bulgaria', + 'BF' => 'Burkina Faso', + 'BI' => 'Burundi', + 'CV' => 'Cabo Verde', + 'KH' => 'Cambodia', + 'CM' => 'Cameroon', + 'CA' => 'Canada', + 'CF' => 'Central African Republic', + 'TD' => 'Chad', + 'CL' => 'Chile', + 'CN' => 'China', + 'CO' => 'Colombia', + 'KM' => 'Comoros', + 'CG' => 'Congo', + 'CD' => 'Congo (DRC)', + 'CR' => 'Costa Rica', + 'CI' => 'Côte d\'Ivoire', + 'HR' => 'Croatia', + 'CU' => 'Cuba', + 'CY' => 'Cyprus', + 'CZ' => 'Czechia', + 'DK' => 'Denmark', + 'DJ' => 'Djibouti', + 'DM' => 'Dominica', + 'DO' => 'Dominican Republic', + 'EC' => 'Ecuador', + 'EG' => 'Egypt', + 'SV' => 'El Salvador', + 'GQ' => 'Equatorial Guinea', + 'ER' => 'Eritrea', + 'EE' => 'Estonia', + 'SZ' => 'Eswatini', + 'ET' => 'Ethiopia', + 'FJ' => 'Fiji', + 'FI' => 'Finland', + 'FR' => 'France', + 'GA' => 'Gabon', + 'GM' => 'Gambia', + 'GE' => 'Georgia', + 'DE' => 'Germany', + 'GH' => 'Ghana', + 'GR' => 'Greece', + 'GD' => 'Grenada', + 'GT' => 'Guatemala', + 'GN' => 'Guinea', + 'GW' => 'Guinea-Bissau', + 'GY' => 'Guyana', + 'HT' => 'Haiti', + 'HN' => 'Honduras', + 'HU' => 'Hungary', + 'IS' => 'Iceland', + 'IN' => 'India', + 'ID' => 'Indonesia', + 'IR' => 'Iran', + 'IQ' => 'Iraq', + 'IE' => 'Ireland', + 'IL' => 'Israel', + 'IT' => 'Italy', + 'JM' => 'Jamaica', + 'JP' => 'Japan', + 'JO' => 'Jordan', + 'KZ' => 'Kazakhstan', + 'KE' => 'Kenya', + 'KI' => 'Kiribati', + 'KP' => 'North Korea', + 'KR' => 'South Korea', + 'KW' => 'Kuwait', + 'KG' => 'Kyrgyzstan', + 'LA' => 'Laos', + 'LV' => 'Latvia', + 'LB' => 'Lebanon', + 'LS' => 'Lesotho', + 'LR' => 'Liberia', + 'LY' => 'Libya', + 'LI' => 'Liechtenstein', + 'LT' => 'Lithuania', + 'LU' => 'Luxembourg', + 'MG' => 'Madagascar', + 'MW' => 'Malawi', + 'MY' => 'Malaysia', + 'MV' => 'Maldives', + 'ML' => 'Mali', + 'MT' => 'Malta', + 'MH' => 'Marshall Islands', + 'MR' => 'Mauritania', + 'MU' => 'Mauritius', + 'MX' => 'Mexico', + 'FM' => 'Micronesia', + 'MD' => 'Moldova', + 'MC' => 'Monaco', + 'MN' => 'Mongolia', + 'ME' => 'Montenegro', + 'MA' => 'Morocco', + 'MZ' => 'Mozambique', + 'MM' => 'Myanmar', + 'NA' => 'Namibia', + 'NR' => 'Nauru', + 'NP' => 'Nepal', + 'NL' => 'Netherlands', + 'NZ' => 'New Zealand', + 'NI' => 'Nicaragua', + 'NE' => 'Niger', + 'NG' => 'Nigeria', + 'MK' => 'North Macedonia', + 'NO' => 'Norway', + 'OM' => 'Oman', + 'PK' => 'Pakistan', + 'PW' => 'Palau', + 'PS' => 'Palestine', + 'PA' => 'Panama', + 'PG' => 'Papua New Guinea', + 'PY' => 'Paraguay', + 'PE' => 'Peru', + 'PH' => 'Philippines', + 'PL' => 'Poland', + 'PT' => 'Portugal', + 'QA' => 'Qatar', + 'RO' => 'Romania', + 'RU' => 'Russia', + 'RW' => 'Rwanda', + 'KN' => 'Saint Kitts and Nevis', + 'LC' => 'Saint Lucia', + 'VC' => 'Saint Vincent and the Grenadines', + 'WS' => 'Samoa', + 'SM' => 'San Marino', + 'ST' => 'São Tomé and Príncipe', + 'SA' => 'Saudi Arabia', + 'SN' => 'Senegal', + 'RS' => 'Serbia', + 'SC' => 'Seychelles', + 'SL' => 'Sierra Leone', + 'SG' => 'Singapore', + 'SK' => 'Slovakia', + 'SI' => 'Slovenia', + 'SB' => 'Solomon Islands', + 'SO' => 'Somalia', + 'ZA' => 'South Africa', + 'SS' => 'South Sudan', + 'ES' => 'Spain', + 'LK' => 'Sri Lanka', + 'SD' => 'Sudan', + 'SR' => 'Suriname', + 'SE' => 'Sweden', + 'CH' => 'Switzerland', + 'SY' => 'Syria', + 'TW' => 'Taiwan', + 'TJ' => 'Tajikistan', + 'TZ' => 'Tanzania', + 'TH' => 'Thailand', + 'TL' => 'Timor-Leste', + 'TG' => 'Togo', + 'TO' => 'Tonga', + 'TT' => 'Trinidad and Tobago', + 'TN' => 'Tunisia', + 'TR' => 'Turkey', + 'TM' => 'Turkmenistan', + 'TV' => 'Tuvalu', + 'UG' => 'Uganda', + 'UA' => 'Ukraine', + 'AE' => 'United Arab Emirates', + 'GB' => 'United Kingdom', + 'US' => 'United States', + 'UY' => 'Uruguay', + 'UZ' => 'Uzbekistan', + 'VU' => 'Vanuatu', + 'VA' => 'Vatican City', + 'VE' => 'Venezuela', + 'VN' => 'Vietnam', + 'YE' => 'Yemen', + 'ZM' => 'Zambia', + 'ZW' => 'Zimbabwe', +]; diff --git a/lang/en/enums.php b/lang/en/enums.php index 2f1f382..57e8c47 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -4,8 +4,8 @@ declare(strict_types=1); return [ 'gender' => [ - 'male' => 'Male', - 'female' => 'Female', + 'male' => 'Erkek', + 'female' => 'Aýal', 'other' => 'Other', ], diff --git a/lang/en/hr.php b/lang/en/hr.php index 4a3390c..96d4cc6 100644 --- a/lang/en/hr.php +++ b/lang/en/hr.php @@ -48,7 +48,7 @@ return [ 'photo' => 'Photo', 'employee_number' => 'Employee Number', 'full_name' => 'Full Name', - 'national_id' => 'National ID', + 'national_id' => 'Nationality', 'gender' => 'Gender', 'birth_date' => 'Birth Date', 'phone' => 'Phone', diff --git a/lang/ru/countries.php b/lang/ru/countries.php new file mode 100644 index 0000000..bd08e62 --- /dev/null +++ b/lang/ru/countries.php @@ -0,0 +1,202 @@ + 'Афганистан', + 'AL' => 'Албания', + 'DZ' => 'Алжир', + 'AD' => 'Андорра', + 'AO' => 'Ангола', + 'AG' => 'Антигуа и Барбуда', + 'AR' => 'Аргентина', + 'AM' => 'Армения', + 'AU' => 'Австралия', + 'AT' => 'Австрия', + 'AZ' => 'Азербайджан', + 'BS' => 'Багамы', + 'BH' => 'Бахрейн', + 'BD' => 'Бангладеш', + 'BB' => 'Барбados', + 'BY' => 'Беларусь', + 'BE' => 'Бельгия', + 'BZ' => 'Белиз', + 'BJ' => 'Бенин', + 'BT' => 'Бутан', + 'BO' => 'Боливия', + 'BA' => 'Босния и Герцеговина', + 'BW' => 'Ботсвана', + 'BR' => 'Бразилия', + 'BN' => 'Бруней', + 'BG' => 'Болгария', + 'BF' => 'Буркина-Фaso', + 'BI' => 'Бурунди', + 'CV' => 'Кabo-Верде', + 'KH' => 'Кambodja', + 'CM' => 'Кamerun', + 'CA' => 'Кanada', + 'CF' => 'Центральноафриканская Республика', + 'TD' => 'Чad', + 'CL' => 'Чili', + 'CN' => 'Китay', + 'CO' => 'Кolumbiya', + 'KM' => 'Коморские Острова', + 'CG' => 'Республика Конgo', + 'CD' => 'Демократическая Республика Конgo', + 'CR' => 'Кosta-Rika', + 'CI' => 'Кôte-d\'Ivoire', + 'HR' => 'Хorvatiya', + 'CU' => 'Кuba', + 'CY' => 'Кipr', + 'CZ' => 'Чехiya', + 'DK' => 'Daniya', + 'DJ' => 'Джibuti', + 'DM' => 'Dominika', + 'DO' => 'Dominikanskaya Respublika', + 'EC' => 'Ekvador', + 'EG' => 'Egipet', + 'SV' => 'Sal\'vador', + 'GQ' => 'Ekvatorial\'naya Gvineya', + 'ER' => 'Eritreya', + 'EE' => 'Estoniya', + 'SZ' => 'Esvatini', + 'ET' => 'Efiopiya', + 'FJ' => 'Fidzhi', + 'FI' => 'Finlyandiya', + 'FR' => 'Frantsiya', + 'GA' => 'Gabon', + 'GM' => 'Gambiya', + 'GE' => 'Gruziya', + 'DE' => 'Germaniya', + 'GH' => 'Gana', + 'GR' => 'Gretsiya', + 'GD' => 'Grenada', + 'GT' => 'Gvatemala', + 'GN' => 'Gvineya', + 'GW' => 'Gvineya-Bisau', + 'GY' => 'Gayana', + 'HT' => 'Gaiti', + 'HN' => 'Gonduras', + 'HU' => 'Vengriya', + 'IS' => 'Islandiya', + 'IN' => 'Indiya', + 'ID' => 'Indoneziya', + 'IR' => 'Иран', + 'IQ' => 'Irak', + 'IE' => 'Irlandiya', + 'IL' => 'Izra il', + 'IT' => 'Italiya', + 'JM' => 'Yamayka', + 'JP' => 'Yaponiya', + 'JO' => 'Iordaniya', + 'KZ' => 'Казахстан', + 'KE' => 'Keniya', + 'KI' => 'Kiribati', + 'KP' => 'KNDR', + 'KR' => 'Respublika Koreya', + 'KW' => 'Kuveyt', + 'KG' => 'Kyrgyzstan', + 'LA' => 'Laos', + 'LV' => 'Latviya', + 'LB' => 'Livan', + 'LS' => 'Lesoto', + 'LR' => 'Liberiya', + 'LY' => 'Liviya', + 'LI' => 'Likhhtenshteyn', + 'LT' => 'Litva', + 'LU' => 'Lyuksemburg', + 'MG' => 'Madagaskar', + 'MW' => 'Malavi', + 'MY' => 'Malayziya', + 'MV' => 'Mal\'divy', + 'ML' => 'Mali', + 'MT' => 'Mal\'ta', + 'MH' => 'Marshallovy Ostrova', + 'MR' => 'Mavritaniya', + 'MU' => 'Mavrikiy', + 'MX' => 'Meksika', + 'FM' => 'Mikroneziya', + 'MD' => 'Moldova', + 'MC' => 'Monako', + 'MN' => 'Mongoliya', + 'ME' => 'Chernogoriya', + 'MA' => 'Marokko', + 'MZ' => 'Mozambik', + 'MM' => 'Myanma', + 'NA' => 'Namibiya', + 'NR' => 'Nauru', + 'NP' => 'Nepal', + 'NL' => 'Niderlandy', + 'NZ' => 'Novaya Zelandiya', + 'NI' => 'Nikaragua', + 'NE' => 'Niger', + 'NG' => 'Nigeriya', + 'MK' => 'Severnaya Makedoniya', + 'NO' => 'Norvegiya', + 'OM' => 'Oman', + 'PK' => 'Pakistan', + 'PW' => 'Palau', + 'PS' => 'Palestina', + 'PA' => 'Panama', + 'PG' => 'Papua — Novaya Gvineya', + 'PY' => 'Paragvay', + 'PE' => 'Peru', + 'PH' => 'Filippiny', + 'PL' => 'Pol\'sha', + 'PT' => 'Portugaliya', + 'QA' => 'Katar', + 'RO' => 'Rumyniya', + 'RU' => 'Россия', + 'RW' => 'Ruanda', + 'KN' => 'Sent-Kits i Nevis', + 'LC' => 'Sent-Lyusiya', + 'VC' => 'Sent-Vinsent i Grenadiny', + 'WS' => 'Samoa', + 'SM' => 'San-Marino', + 'ST' => 'San-Tome i Prinsipi', + 'SA' => 'Saudovskaya Araviya', + 'SN' => 'Senegal', + 'RS' => 'Serbiya', + 'SC' => 'Seyshely', + 'SL' => 'Serra-Leone', + 'SG' => 'Singapur', + 'SK' => 'Slovakiya', + 'SI' => 'Sloveniya', + 'SB' => 'Solomonovy Ostrova', + 'SO' => 'Somali', + 'ZA' => 'Yuzhno-Afrikanskaya Respublika', + 'SS' => 'Yuzhnyy Sudan', + 'ES' => 'Ispaniya', + 'LK' => 'Shri-Lanka', + 'SD' => 'Sudan', + 'SR' => 'Surinam', + 'SE' => 'Shvetsiya', + 'CH' => 'Shveytsariya', + 'SY' => 'Siriya', + 'TW' => 'Tayvan', + 'TJ' => 'Tadzhikistan', + 'TZ' => 'Tanzaniya', + 'TH' => 'Tailand', + 'TL' => 'Timor-Leste', + 'TG' => 'Togo', + 'TO' => 'Tonga', + 'TT' => 'Trinidad i Tobago', + 'TN' => 'Tunis', + 'TR' => 'Турция', + 'TM' => 'Туркmenistan', + 'TV' => 'Tuvalu', + 'UG' => 'Uganda', + 'UA' => 'Ukraina', + 'AE' => 'OAE', + 'GB' => 'Velikobritaniya', + 'US' => 'SShA', + 'UY' => 'Urugvay', + 'UZ' => 'Uzbekistan', + 'VU' => 'Vanuatu', + 'VA' => 'Vatikan', + 'VE' => 'Venesuela', + 'VN' => 'Vetnam', + 'YE' => 'Yemen', + 'ZM' => 'Zambiya', + 'ZW' => 'Zimbabve', +]; diff --git a/lang/ru/hr.php b/lang/ru/hr.php index 85bb949..d5e642e 100644 --- a/lang/ru/hr.php +++ b/lang/ru/hr.php @@ -48,7 +48,7 @@ return [ 'photo' => 'Фото', 'employee_number' => 'Табельный номер', 'full_name' => 'ФИО', - 'national_id' => 'Национальный ID', + 'national_id' => 'Гражданство', 'gender' => 'Пол', 'birth_date' => 'Дата рождения', 'phone' => 'Телефон', diff --git a/lang/tk/countries.php b/lang/tk/countries.php new file mode 100644 index 0000000..8ad0f39 --- /dev/null +++ b/lang/tk/countries.php @@ -0,0 +1,202 @@ + 'Owganystan', + 'AL' => 'Albaniýa', + 'DZ' => 'Aljir', + 'AD' => 'Andorra', + 'AO' => 'Angola', + 'AG' => 'Antigua and Barbuda', + 'AR' => 'Argentina', + 'AM' => 'Ermenistan', + 'AU' => 'Awstraliýa', + 'AT' => 'Awstriýa', + 'AZ' => 'Azerbaýjan', + 'BS' => 'Bagama adalary', + 'BH' => 'Bahreýn', + 'BD' => 'Bangladesh', + 'BB' => 'Barbados', + 'BY' => 'Belarus', + 'BE' => 'Belgiýa', + 'BZ' => 'Belize', + 'BJ' => 'Benin', + 'BT' => 'Butan', + 'BO' => 'Boliwiýa', + 'BA' => 'Bosniýa we Gersegowina', + 'BW' => 'Botswana', + 'BR' => 'Braziliýa', + 'BN' => 'Bruneý', + 'BG' => 'Bolgariýa', + 'BF' => 'Burkina Faso', + 'BI' => 'Burundi', + 'CV' => 'Cabo Verde', + 'KH' => 'Kamboja', + 'CM' => 'Cameroon', + 'CA' => 'Kanada', + 'CF' => 'Central African Republic', + 'TD' => 'Chad', + 'CL' => 'Çili', + 'CN' => 'Hytaý', + 'CO' => 'Kolumbiýa', + 'KM' => 'Comoros', + 'CG' => 'Congo', + 'CD' => 'Congo (DRC)', + 'CR' => 'Kosta-Rika', + 'CI' => 'Côte d\'Ivoire', + 'HR' => 'Horwatiýa', + 'CU' => 'Kuba', + 'CY' => 'Kipr', + 'CZ' => 'Çeh Respublikasy', + 'DK' => 'Daniýa', + 'DJ' => 'Djibouti', + 'DM' => 'Dominica', + 'DO' => 'Dominikan Respublikasy', + 'EC' => 'Ekwador', + 'EG' => 'Müsür', + 'SV' => 'Salwador', + 'GQ' => 'Equatorial Guinea', + 'ER' => 'Eritrea', + 'EE' => 'Estoniýa', + 'SZ' => 'Eswatini', + 'ET' => 'Efiopiýa', + 'FJ' => 'Fiji', + 'FI' => 'Finlandiýa', + 'FR' => 'Fransiýa', + 'GA' => 'Gabon', + 'GM' => 'Gambia', + 'GE' => 'Gruziýa', + 'DE' => 'Germaniýa', + 'GH' => 'Ghana', + 'GR' => 'Gresiýa', + 'GD' => 'Grenada', + 'GT' => 'Gwatemala', + 'GN' => 'Guinea', + 'GW' => 'Guinea-Bissau', + 'GY' => 'Gaýana', + 'HT' => 'Gaiti', + 'HN' => 'Gonduras', + 'HU' => 'Wengriýa', + 'IS' => 'Islandiýa', + 'IN' => 'Hindistan', + 'ID' => 'Indoneziýa', + 'IR' => 'Eýran', + 'IQ' => 'Yrak', + 'IE' => 'Irlandiýa', + 'IL' => 'Ysrail', + 'IT' => 'Italiýa', + 'JM' => 'Ýamaýka', + 'JP' => 'Ýaponiýa', + 'JO' => 'Iordaniýa', + 'KZ' => 'Gazagystan', + 'KE' => 'Keniýa', + 'KI' => 'Kiribati', + 'KP' => 'Demokratik Halk Respublikasy', + 'KR' => 'Günorta Koreýa', + 'KW' => 'Kuweýt', + 'KG' => 'Gyrgyzystan', + 'LA' => 'Laos', + 'LV' => 'Latwiýa', + 'LB' => 'Liwan', + 'LS' => 'Lesotho', + 'LR' => 'Liberia', + 'LY' => 'Liwiýa', + 'LI' => 'Lihtenshteýn', + 'LT' => 'Litwa', + 'LU' => 'Lýuksemburg', + 'MG' => 'Madagascar', + 'MW' => 'Malawi', + 'MY' => 'Malaýziýa', + 'MV' => 'Maldiwler', + 'ML' => 'Mali', + 'MT' => 'Malta', + 'MH' => 'Marşall adalary', + 'MR' => 'Mauritania', + 'MU' => 'Mauritius', + 'MX' => 'Meksika', + 'FM' => 'Mikroneziýa', + 'MD' => 'Moldowa', + 'MC' => 'Monako', + 'MN' => 'Mongoliýa', + 'ME' => 'Chernogoriýa', + 'MA' => 'Marokko', + 'MZ' => 'Mozambique', + 'MM' => 'Mýanma', + 'NA' => 'Namibia', + 'NR' => 'Nauru', + 'NP' => 'Nepal', + 'NL' => 'Gollandiýa', + 'NZ' => 'Täze Zelandiýa', + 'NI' => 'Nikaragua', + 'NE' => 'Niger', + 'NG' => 'Nigeriýa', + 'MK' => 'Demirgazyk Makedoniýa', + 'NO' => 'Norwegiýa', + 'OM' => 'Oman', + 'PK' => 'Pakistan', + 'PW' => 'Palau', + 'PS' => 'Palestina', + 'PA' => 'Panama', + 'PG' => 'Papua — Täze Gwineýa', + 'PY' => 'Paragwaý', + 'PE' => 'Peru', + 'PH' => 'Filippinler', + 'PL' => 'Polşa', + 'PT' => 'Portugaliýa', + 'QA' => 'Katar', + 'RO' => 'Rumyniýa', + 'RU' => 'Rusiya', + 'RW' => 'Rwanda', + 'KN' => 'Saint Kitts and Nevis', + 'LC' => 'Saint Lucia', + 'VC' => 'Saint Vincent and the Grenadines', + 'WS' => 'Samoa', + 'SM' => 'San-Marino', + 'ST' => 'São Tomé and Príncipe', + 'SA' => 'Saud Arabystany', + 'SN' => 'Senegal', + 'RS' => 'Serbiýa', + 'SC' => 'Seychelles', + 'SL' => 'Sierra Leone', + 'SG' => 'Singapur', + 'SK' => 'Slowakiýa', + 'SI' => 'Slovenia', + 'SB' => 'Solomon adalary', + 'SO' => 'Somalia', + 'ZA' => 'Günorta Afrika', + 'SS' => 'South Sudan', + 'ES' => 'Ispaniýa', + 'LK' => 'Shri-Lanka', + 'SD' => 'Sudan', + 'SR' => 'Surinam', + 'SE' => 'Shwesiýa', + 'CH' => 'Shweýsariýa', + 'SY' => 'Siriýa', + 'TW' => 'Taýwan', + 'TJ' => 'Täjikistan', + 'TZ' => 'Tanzania', + 'TH' => 'Taýland', + 'TL' => 'Timor-Leste', + 'TG' => 'Togo', + 'TO' => 'Tonga', + 'TT' => 'Trinidad we Tobago', + 'TN' => 'Tunis', + 'TR' => 'Türkiýe', + 'TM' => 'Türkmenistan', + 'TV' => 'Tuwalu', + 'UG' => 'Uganda', + 'UA' => 'Ukraina', + 'AE' => 'Birleşik Arap Emirlikleri', + 'GB' => 'Beýik Britaniýa', + 'US' => 'Amerika Birleşik Ştatlary', + 'UY' => 'Urugwaý', + 'UZ' => 'Ozbekistan', + 'VU' => 'Wanuatu', + 'VA' => 'Watikan', + 'VE' => 'Wenesuela', + 'VN' => 'Wýetnam', + 'YE' => 'Ýemen', + 'ZM' => 'Zambia', + 'ZW' => 'Zimbabwe', +]; diff --git a/lang/tk/hr.php b/lang/tk/hr.php index af1e352..f2d6030 100644 --- a/lang/tk/hr.php +++ b/lang/tk/hr.php @@ -48,7 +48,7 @@ return [ 'photo' => 'Surat', 'employee_number' => 'Işgär belgisi', 'full_name' => 'Doly ady', - 'national_id' => 'Milli ID', + 'national_id' => 'Raýatlyk', 'gender' => 'Jyns', 'birth_date' => 'Doglan senesi', 'phone' => 'Telefon', diff --git a/tests/Feature/Import/EmployeeImportTest.php b/tests/Feature/Import/EmployeeImportTest.php index 5dafd4f..f9ad94e 100644 --- a/tests/Feature/Import/EmployeeImportTest.php +++ b/tests/Feature/Import/EmployeeImportTest.php @@ -22,13 +22,13 @@ beforeEach(function (): void { it('skips duplicate employees during import', function (): void { Employee::factory()->create([ 'employee_number' => 'EMP-90001', - 'national_id' => '1234567890', + 'national_id' => 'TM', ]); $rows = [[ 'employee_number' => 'EMP-90001', 'full_name' => 'Duplicate Employee', - 'national_id' => '1234567890', + 'national_id' => 'TM', 'department' => 'Engineering', 'position' => 'Developer', 'shift' => 'Day Shift', @@ -68,6 +68,29 @@ it('imports valid employee rows', function (): void { ]); }); +it('imports valid employee rows without an employee number', function (): void { + $rows = [[ + 'full_name' => 'Auto Number Employee', + 'gender' => 'male', + 'birth_date' => '1992-02-20', + 'department' => 'Engineering', + 'position' => 'Developer', + 'shift' => 'Day Shift', + 'hire_date' => '2024-04-01', + ]]; + + $result = $this->importService->import(ImportType::Employees, $rows); + + expect($result->imported)->toBe(1) + ->and($result->skipped)->toBe(0) + ->and($result->failed)->toBe(0); + + $employee = Employee::query()->where('full_name', 'Auto Number Employee')->first(); + + expect($employee)->not->toBeNull() + ->and($employee->employee_number)->toBe('EMP-00001'); +}); + it('reports validation errors for invalid employee rows', function (): void { $rows = [[ 'employee_number' => '', @@ -93,8 +116,7 @@ it('validates required fields and unknown references', function (): void { $errors = $this->transformer->validate($dto); - expect($errors)->toContain('Employee number is required.') - ->and($errors)->toContain('Full name is required.') + expect($errors)->toContain('Full name is required.') ->and($errors)->toContain('Department "Missing Department" was not found.') ->and($errors)->toContain('Position "Missing Position" was not found.') ->and($errors)->toContain('Shift "Missing Shift" was not found.'); @@ -103,7 +125,7 @@ it('validates required fields and unknown references', function (): void { it('detects duplicates by employee number or national id', function (): void { Employee::factory()->create([ 'employee_number' => 'EMP-80001', - 'national_id' => '9988776655', + 'national_id' => 'TM', ]); $duplicateByNumber = EmployeeRowDto::fromMappedRow([ @@ -114,7 +136,7 @@ it('detects duplicates by employee number or national id', function (): void { $duplicateByNationalId = EmployeeRowDto::fromMappedRow([ 'employee_number' => 'EMP-80002', 'full_name' => 'Another Person', - 'national_id' => '9988776655', + 'national_id' => 'TM', ]); expect($this->transformer->isDuplicate($duplicateByNumber))->toBeTrue() diff --git a/tests/Feature/LocalizationTest.php b/tests/Feature/LocalizationTest.php index 78b3ff3..dcf27ce 100644 --- a/tests/Feature/LocalizationTest.php +++ b/tests/Feature/LocalizationTest.php @@ -35,6 +35,19 @@ it('returns translated navigation group labels', function (): void { expect(NavigationGroup::LeaveManagement->getLabel())->toBe('Dynç alyş dolandyryşy'); }); +it('returns translated filament navigation group labels at request time', function (): void { + $admin = createAdminUser(['locale' => 'tk']); + + $this->actingAs($admin); + + App::setLocale('tk'); + + $groups = filament()->getNavigation(); + $labels = collect($groups)->map->getLabel()->filter()->values()->all(); + + expect($labels)->toContain('Gurama', 'Işgärler', 'Dynç alyş dolandyryşy', 'Ýazgylar'); +}); + it('returns translated filament resource navigation labels', function (): void { App::setLocale('tk'); diff --git a/tests/Feature/Models/EmployeeTest.php b/tests/Feature/Models/EmployeeTest.php index ecf97ff..c402398 100644 --- a/tests/Feature/Models/EmployeeTest.php +++ b/tests/Feature/Models/EmployeeTest.php @@ -84,6 +84,32 @@ it('defines expected employee relationships', function (): void { ->and($employee->documents)->toHaveCount(1); }); +it('generates an employee number on save when number is blank', function (): void { + $employee = Employee::factory()->create([ + 'employee_number' => '', + ]); + + expect($employee->employee_number)->toBe('EMP-00001'); +}); + +it('does not overwrite an existing employee number on update', function (): void { + $employee = Employee::factory()->create([ + 'employee_number' => 'EMP-CUSTOM', + ]); + + $employee->update(['full_name' => 'Updated Name']); + + expect($employee->fresh()->employee_number)->toBe('EMP-CUSTOM'); +}); + +it('increments employee numbers sequentially', function (): void { + $first = Employee::factory()->create(['employee_number' => '']); + $second = Employee::factory()->create(['employee_number' => '']); + + expect($first->employee_number)->toBe('EMP-00001') + ->and($second->employee_number)->toBe('EMP-00002'); +}); + it('enforces a unique employee number', function (): void { Employee::factory()->create(['employee_number' => 'EMP-UNIQUE']); diff --git a/tests/Feature/Services/EmployeeServiceTest.php b/tests/Feature/Services/EmployeeServiceTest.php index 542d20e..33e82e4 100644 --- a/tests/Feature/Services/EmployeeServiceTest.php +++ b/tests/Feature/Services/EmployeeServiceTest.php @@ -26,7 +26,6 @@ it('creates an employee with the provided data', function (): void { $shift = Shift::factory()->create(); $employee = $this->service->create([ - 'employee_number' => 'EMP-10001', 'full_name' => 'John Doe', 'gender' => 'male', 'birth_date' => '1990-05-15', @@ -39,16 +38,37 @@ it('creates an employee with the provided data', function (): void { ]); expect($employee)->toBeInstanceOf(Employee::class) - ->and($employee->employee_number)->toBe('EMP-10001') + ->and($employee->employee_number)->toBe('EMP-00001') ->and($employee->full_name)->toBe('John Doe') ->and($employee->employment_status)->toBe(EmploymentStatus::Active); $this->assertDatabaseHas('employees', [ - 'employee_number' => 'EMP-10001', + 'employee_number' => 'EMP-00001', 'full_name' => 'John Doe', ]); }); +it('creates an employee with an explicit employee number', function (): void { + $department = Department::factory()->create(); + $position = Position::factory()->create(); + $shift = Shift::factory()->create(); + + $employee = $this->service->create([ + 'employee_number' => 'EMP-10001', + 'full_name' => 'Jane Doe', + 'gender' => 'female', + 'birth_date' => '1991-06-20', + 'phone' => '+993 61 92 92 48', + 'department_id' => $department->id, + 'position_id' => $position->id, + 'shift_id' => $shift->id, + 'employment_status' => EmploymentStatus::Active, + 'hire_date' => '2024-02-01', + ]); + + expect($employee->employee_number)->toBe('EMP-10001'); +}); + it('terminates an employee and notifies hr managers', function (): void { $employee = Employee::factory()->active()->create(); $terminationDate = Carbon::parse('2025-06-30');