getTranslation('title', app()->getLocale()); } public static function form(Schema $schema): Schema { return $schema->components([ Section::make()->schema([ TranslatableTabs::make(fn (string $locale, bool $isDefault) => [ TextInput::make("title.{$locale}") ->label(__('panel.calls.fields.title')) ->required($isDefault) ->maxLength(255) ->columnSpanFull() ->live(onBlur: true) ->afterStateUpdated(function (?string $state, callable $get, callable $set) use ($locale) { if (blank($get("slug.{$locale}"))) { $set("slug.{$locale}", Str::slug((string) $state)); } }), TextInput::make("slug.{$locale}") ->label(__('panel.calls.fields.slug')) ->required($isDefault) ->maxLength(255) ->columnSpanFull() ->rules(fn ($record) => [new UniqueTranslatedSlug(Call::class, $record?->id)]), Textarea::make("description.{$locale}") ->label(__('panel.calls.fields.description')) ->rows(4) ->columnSpanFull(), // Unpaid work needs an explicit exchange. Saying it out // loud attracts more people than leaving it implied. Textarea::make("what_you_get.{$locale}") ->label(__('panel.calls.fields.what_you_get')) ->helperText(__('panel.calls.fields.what_you_get_hint')) ->rows(3) ->columnSpanFull(), ]), ]), Section::make(__('panel.calls.sections.commitment')) ->description(__('panel.calls.sections.commitment_hint')) ->columns(3) ->schema([ TextInput::make('hours_per_week') ->label(__('panel.calls.fields.hours_per_week')) ->numeric() ->minValue(1) ->maxValue(40) ->suffix('h'), TextInput::make('duration_weeks') ->label(__('panel.calls.fields.duration_weeks')) ->numeric() ->minValue(1), Select::make('seniority') ->label(__('panel.calls.fields.seniority')) ->options([ 'any' => __('panel.calls.seniority.any'), 'junior' => __('panel.calls.seniority.junior'), 'mid' => __('panel.calls.seniority.mid'), 'senior' => __('panel.calls.seniority.senior'), ]), ]), Section::make() ->columns(2) ->schema([ Select::make('project_id') ->label(__('panel.calls.fields.project')) ->relationship('project', 'title->'.config('locales.default')) ->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title')) ->searchable() ->preload() ->helperText(__('panel.calls.fields.project_hint')), Select::make('status') ->label(__('panel.calls.fields.status')) ->options(CallStatus::class) ->default(CallStatus::Open) ->required(), Select::make('technologies') ->label(__('panel.calls.fields.technologies')) ->relationship('technologies', 'name') ->multiple() ->preload() ->searchable() ->columnSpanFull(), DatePicker::make('opens_at')->label(__('panel.calls.fields.opens_at')), DatePicker::make('closes_at')->label(__('panel.calls.fields.closes_at')), ]), ]); } public static function table(Table $table): Table { $locale = app()->getLocale(); return $table ->defaultSort('created_at', 'desc') ->columns([ TextColumn::make('title') ->label(__('panel.calls.fields.title')) ->weight('medium') ->wrap() ->searchable(query: fn ($q, string $search) => $q ->where("title->{$locale}", 'like', "%{$search}%")), TextColumn::make('status') ->label(__('panel.calls.fields.status')) ->badge() ->sortable(), TextColumn::make('project.title') ->label(__('panel.calls.fields.project')) ->badge() ->color('gray') ->placeholder(__('panel.calls.no_project')), // The number that does the work on the public site. TextColumn::make('hours_per_week') ->label(__('panel.calls.fields.commitment')) ->formatStateUsing(fn ($state, Call $record) => $state ? trans_choice('panel.calls.commitment_summary', $record->duration_weeks ?? 0, [ 'hours' => $state, 'weeks' => $record->duration_weeks, ]) : '—') ->badge() ->color('info'), TextColumn::make('technologies.name') ->label(__('panel.calls.fields.technologies')) ->badge() ->color('gray') ->limitList(3) ->toggleable(), TextColumn::make('closes_at') ->label(__('panel.calls.fields.closes_at')) ->date() ->placeholder('—') ->sortable(), ]) ->filters([ SelectFilter::make('status') ->label(__('panel.calls.fields.status')) ->options(CallStatus::options()) ->multiple(), SelectFilter::make('technologies') ->label(__('panel.calls.fields.technologies')) ->relationship('technologies', 'name') ->multiple() ->preload(), ]) ->recordActions([ // A near-copy is a real workflow: the third similar service, // next year's call. The slug is cleared because two records // cannot answer at the same URL. ReplicateAction::make() ->label(__('panel.actions.duplicate')) ->icon('heroicon-o-document-duplicate') ->excludeAttributes(['slug', 'published_at', 'unpublish_at']) ->modalHeading(__('panel.actions.duplicate_heading')) ->successNotificationTitle(__('panel.actions.duplicated')), PreviewAction::for('call'), EditAction::make(), DeleteAction::make(), ]) ->toolbarActions([ BulkActionGroup::make([DeleteBulkAction::make()]), ]); } public static function getRelations(): array { return [ RevisionsRelationManager::class, ApplicationsRelationManager::class, ]; } /** * @return array */ public static function getGloballySearchableAttributes(): array { return ['title->'.app()->getLocale()]; } public static function getPages(): array { return [ 'index' => ListCalls::route('/'), 'create' => CreateCall::route('/create'), 'edit' => EditCall::route('/{record}/edit'), ]; } /** * How many contributor slots are currently open — the number that tells * the collective whether the site is doing its job. */ public static function getNavigationBadge(): ?string { $open = static::getModel()::query()->open()->count(); return $open > 0 ? (string) $open : null; } public static function getNavigationBadgeColor(): ?string { return 'success'; } }