fix(departments): fix contact info editing, cache invalidation, and pivot data display

- User model: add missing pivot columns (service_email, service_phone, cabinet, sort) to departments_work() and departments_teach() withPivot
- ListDepartmentWorkersAction/TeachersAction: fix where() to wherePivot() for position filters
- Dashboard Workers/Teachers Index.vue: display validation errors in add/edit modals
- Dashboard Workers/Teachers Index.vue: add links to client person pages
- DepartmentCacheService: clear person_* cache when department data changes
- Filament WorkersRelationManager/TeachersRelationManager: fix pivot columns in table (pivot.position etc), add cache clearing on edit/detach/bulk actions
This commit is contained in:
F4ilji
2026-09-18 01:32:27 +05:00
parent e0e80fc430
commit bf77cb634c
8 changed files with 73 additions and 20 deletions
@@ -17,7 +17,7 @@ class ListDepartmentTeachersAction
// Фильтр по должности // Фильтр по должности
if (!empty($filters['position'])) { if (!empty($filters['position'])) {
$query->where('teaching_position', 'like', '%' . $filters['position'] . '%'); $query->wherePivot('teaching_position', 'like', '%' . $filters['position'] . '%');
} }
$teachers = $query->orderBy('teachers_departments.sort')->paginate(20)->withQueryString(); $teachers = $query->orderBy('teachers_departments.sort')->paginate(20)->withQueryString();
@@ -18,7 +18,7 @@ class ListDepartmentWorkersAction
// Фильтр по должности // Фильтр по должности
if (!empty($filters['position'])) { if (!empty($filters['position'])) {
$query->where('position', 'like', '%' . $filters['position'] . '%'); $query->wherePivot('position', 'like', '%' . $filters['position'] . '%');
} }
$workers = $query->orderBy('workers_departments.sort')->paginate(20)->withQueryString(); $workers = $query->orderBy('workers_departments.sort')->paginate(20)->withQueryString();
+8 -4
View File
@@ -12,7 +12,7 @@ use BezhanSalleh\FilamentShield\Support\Utils;
use BezhanSalleh\FilamentShield\Traits\HasPanelShield; use BezhanSalleh\FilamentShield\Traits\HasPanelShield;
use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\FilamentUser;
use Filament\Tables\Columns\Layout\Panel; use Filament\Tables\Columns\Layout\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Ship\Traits\HasContainerFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
@@ -23,7 +23,9 @@ use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements FilamentUser, SeoTitleInterface, SeoDescriptionInterface class User extends Authenticatable implements FilamentUser, SeoTitleInterface, SeoDescriptionInterface
{ {
use HasApiTokens, HasFactory, Notifiable, HasRoles, HasPanelShield, HasSeo; use HasApiTokens, HasContainerFactory, Notifiable, HasRoles, HasPanelShield, HasSeo;
protected static string $factory = \Database\Factories\UserFactory::class;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -65,12 +67,14 @@ class User extends Authenticatable implements FilamentUser, SeoTitleInterface, S
public function departments_work(): BelongsToMany public function departments_work(): BelongsToMany
{ {
return $this->belongsToMany(Department::class, 'workers_departments')->withPivot(['position']); return $this->belongsToMany(Department::class, 'workers_departments')
->withPivot(['position', 'sort', 'service_email', 'service_phone', 'cabinet']);
} }
public function departments_teach(): BelongsToMany public function departments_teach(): BelongsToMany
{ {
return $this->belongsToMany(Department::class, 'teachers_departments')->withPivot(['teaching_position']); return $this->belongsToMany(Department::class, 'teachers_departments')
->withPivot(['teaching_position', 'sort', 'service_email', 'service_phone', 'cabinet']);
} }
public function divisions(): BelongsToMany public function divisions(): BelongsToMany
@@ -70,18 +70,18 @@ class TeachersRelationManager extends RelationManager
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('teaching_position') Tables\Columns\TextColumn::make('pivot.teaching_position')
->label('Должность') ->label('Должность')
->searchable() ->searchable()
->sortable() ->sortable()
->wrap(), ->wrap(),
Tables\Columns\TextColumn::make('service_email') Tables\Columns\TextColumn::make('pivot.service_email')
->label('Почта') ->label('Почта')
->searchable() ->searchable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('cabinet') Tables\Columns\TextColumn::make('pivot.cabinet')
->label('Кабинет') ->label('Кабинет')
->sortable() ->sortable()
->toggleable(), ->toggleable(),
@@ -144,14 +144,20 @@ class TeachersRelationManager extends RelationManager
->actions([ ->actions([
Tables\Actions\EditAction::make() Tables\Actions\EditAction::make()
->iconButton() ->iconButton()
->tooltip('Редактировать'), ->tooltip('Редактировать')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
Tables\Actions\DetachAction::make() Tables\Actions\DetachAction::make()
->iconButton() ->iconButton()
->tooltip('Убрать с кафедры') ->tooltip('Убрать с кафедры')
->modalHeading('Удаление связи') ->modalHeading('Удаление связи')
->modalSubmitActionLabel('Убрать') ->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать этого преподавателя с кафедры?'), ->modalDescription('Вы уверены, что хотите убрать этого преподавателя с кафедры?')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
@@ -159,7 +165,10 @@ class TeachersRelationManager extends RelationManager
->label('Убрать выбранных') ->label('Убрать выбранных')
->modalHeading('Удаление связей') ->modalHeading('Удаление связей')
->modalSubmitActionLabel('Убрать') ->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать выбранных преподавателей с кафедры?'), ->modalDescription('Вы уверены, что хотите убрать выбранных преподавателей с кафедры?')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
]), ]),
]) ])
->emptyStateActions([ ->emptyStateActions([
@@ -70,18 +70,18 @@ class WorkersRelationManager extends RelationManager
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('position') Tables\Columns\TextColumn::make('pivot.position')
->label('Должность') ->label('Должность')
->searchable() ->searchable()
->sortable() ->sortable()
->wrap(), ->wrap(),
Tables\Columns\TextColumn::make('service_email') Tables\Columns\TextColumn::make('pivot.service_email')
->label('Почта') ->label('Почта')
->searchable() ->searchable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('cabinet') Tables\Columns\TextColumn::make('pivot.cabinet')
->label('Кабинет') ->label('Кабинет')
->sortable() ->sortable()
->toggleable(), ->toggleable(),
@@ -144,14 +144,20 @@ class WorkersRelationManager extends RelationManager
->actions([ ->actions([
Tables\Actions\EditAction::make() Tables\Actions\EditAction::make()
->iconButton() ->iconButton()
->tooltip('Редактировать'), ->tooltip('Редактировать')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
Tables\Actions\DetachAction::make() Tables\Actions\DetachAction::make()
->iconButton() ->iconButton()
->tooltip('Убрать с кафедры') ->tooltip('Убрать с кафедры')
->modalHeading('Удаление связи') ->modalHeading('Удаление связи')
->modalSubmitActionLabel('Убрать') ->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать этого сотрудника с кафедры?'), ->modalDescription('Вы уверены, что хотите убрать этого сотрудника с кафедры?')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
@@ -159,7 +165,10 @@ class WorkersRelationManager extends RelationManager
->label('Убрать выбранных') ->label('Убрать выбранных')
->modalHeading('Удаление связей') ->modalHeading('Удаление связей')
->modalSubmitActionLabel('Убрать') ->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать выбранных сотрудников с кафедры?'), ->modalDescription('Вы уверены, что хотите убрать выбранных сотрудников с кафедры?')
->before(function () {
app(DepartmentCacheService::class)->clearAllCacheByModel();
}),
]), ]),
]) ])
->emptyStateActions([ ->emptyStateActions([
@@ -21,6 +21,7 @@ class DepartmentCacheService extends AbstractCacheService implements CacheInterf
{ {
$this->clearCacheByPrefix(CacheKeys::DEPARTMENT_PREFIX->value.'*'); $this->clearCacheByPrefix(CacheKeys::DEPARTMENT_PREFIX->value.'*');
$this->clearCacheByPrefix(CacheKeys::DEPARTMENTS_PREFIX->value.'*'); $this->clearCacheByPrefix(CacheKeys::DEPARTMENTS_PREFIX->value.'*');
$this->clearCacheByPrefix(CacheKeys::USER_PREFIX->value.'*');
} }
public function getCachedData(string $key) public function getCachedData(string $key)
@@ -41,7 +41,11 @@
<tbody class="divide-y divide-line-2 bg-layer"> <tbody class="divide-y divide-line-2 bg-layer">
<tr v-for="teacher in teachers.data" :key="teacher.id" class="hover:bg-muted/20"> <tr v-for="teacher in teachers.data" :key="teacher.id" class="hover:bg-muted/20">
<td class="px-4 py-3 text-sm font-medium text-foreground"> <td class="px-4 py-3 text-sm font-medium text-foreground">
{{ teacher.name }} <span
class="text-primary hover:underline cursor-pointer"
title="Открыть на сайте"
@click="openPerson(teacher.slug)"
>{{ teacher.name }}</span>
</td> </td>
<td class="px-4 py-3 text-sm text-foreground"> <td class="px-4 py-3 text-sm text-foreground">
{{ teacher.pivot.teaching_position }} {{ teacher.pivot.teaching_position }}
@@ -124,6 +128,7 @@
{{ user.name }} {{ user.name }}
</option> </option>
</select> </select>
<p v-if="$page.props.errors.user_id" class="mt-1 text-sm text-danger">{{ $page.props.errors.user_id }}</p>
</div> </div>
<div> <div>
@@ -138,6 +143,7 @@
placeholder="Например: Профессор" placeholder="Например: Профессор"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.teaching_position" class="mt-1 text-sm text-danger">{{ $page.props.errors.teaching_position }}</p>
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
@@ -152,6 +158,7 @@
placeholder="example@university.ru" placeholder="example@university.ru"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.service_email" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_email }}</p>
</div> </div>
<div> <div>
@@ -165,6 +172,7 @@
placeholder="+7 (XXX) XXX-XX-XX" placeholder="+7 (XXX) XXX-XX-XX"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.service_phone" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_phone }}</p>
</div> </div>
</div> </div>
@@ -179,6 +187,7 @@
placeholder="Например: 305а" placeholder="Например: 305а"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.cabinet" class="mt-1 text-sm text-danger">{{ $page.props.errors.cabinet }}</p>
</div> </div>
<div class="flex items-center justify-end gap-3 pt-4"> <div class="flex items-center justify-end gap-3 pt-4">
@@ -205,11 +214,13 @@
<script> <script>
import DashboardIcon from '../../Components/DashboardIcon.vue'; import DashboardIcon from '../../Components/DashboardIcon.vue';
import { Link } from '@inertiajs/vue3';
export default { export default {
name: 'DepartmentTeachers', name: 'DepartmentTeachers',
components: { components: {
DashboardIcon, DashboardIcon,
Link,
}, },
props: { props: {
department: { type: Object, required: true }, department: { type: Object, required: true },
@@ -284,6 +295,10 @@ export default {
service_phone: '', service_phone: '',
cabinet: '' cabinet: ''
}; };
this.$page.props.errors = {};
},
openPerson(slug) {
window.open(route('client.person.show', slug), '_blank');
} }
} }
} }
@@ -41,7 +41,11 @@
<tbody class="divide-y divide-line-2 bg-layer"> <tbody class="divide-y divide-line-2 bg-layer">
<tr v-for="worker in workers.data" :key="worker.id" class="hover:bg-muted/20"> <tr v-for="worker in workers.data" :key="worker.id" class="hover:bg-muted/20">
<td class="px-4 py-3 text-sm font-medium text-foreground"> <td class="px-4 py-3 text-sm font-medium text-foreground">
{{ worker.name }} <span
class="text-primary hover:underline cursor-pointer"
title="Открыть на сайте"
@click="openPerson(worker.slug)"
>{{ worker.name }}</span>
</td> </td>
<td class="px-4 py-3 text-sm text-foreground"> <td class="px-4 py-3 text-sm text-foreground">
{{ worker.pivot.position }} {{ worker.pivot.position }}
@@ -124,6 +128,7 @@
{{ user.name }} {{ user.name }}
</option> </option>
</select> </select>
<p v-if="$page.props.errors.user_id" class="mt-1 text-sm text-danger">{{ $page.props.errors.user_id }}</p>
</div> </div>
<div> <div>
@@ -138,6 +143,7 @@
placeholder="Например: Заведующий кафедрой" placeholder="Например: Заведующий кафедрой"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.position" class="mt-1 text-sm text-danger">{{ $page.props.errors.position }}</p>
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
@@ -152,6 +158,7 @@
placeholder="example@university.ru" placeholder="example@university.ru"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.service_email" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_email }}</p>
</div> </div>
<div> <div>
@@ -165,6 +172,7 @@
placeholder="+7 (XXX) XXX-XX-XX" placeholder="+7 (XXX) XXX-XX-XX"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.service_phone" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_phone }}</p>
</div> </div>
</div> </div>
@@ -179,6 +187,7 @@
placeholder="Например: 305а" placeholder="Например: 305а"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/> />
<p v-if="$page.props.errors.cabinet" class="mt-1 text-sm text-danger">{{ $page.props.errors.cabinet }}</p>
</div> </div>
<div class="flex items-center justify-end gap-3 pt-4"> <div class="flex items-center justify-end gap-3 pt-4">
@@ -205,11 +214,13 @@
<script> <script>
import DashboardIcon from '../../Components/DashboardIcon.vue'; import DashboardIcon from '../../Components/DashboardIcon.vue';
import { Link } from '@inertiajs/vue3';
export default { export default {
name: 'DepartmentWorkers', name: 'DepartmentWorkers',
components: { components: {
DashboardIcon, DashboardIcon,
Link,
}, },
props: { props: {
department: { type: Object, required: true }, department: { type: Object, required: true },
@@ -284,6 +295,10 @@ export default {
service_phone: '', service_phone: '',
cabinet: '' cabinet: ''
}; };
this.$page.props.errors = {};
},
openPerson(slug) {
window.open(route('client.person.show', slug), '_blank');
} }
} }
} }