Files
ntspi-app/resources/js/Pages/Dashboard/Departments/Workers/Index.vue
T
F4ilji bf77cb634c 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
2026-09-18 01:32:27 +05:00

306 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="space-y-4">
<!-- Header -->
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-foreground">Сотрудники кафедры</h3>
<button
type="button"
@click="showAddModal = true"
class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-primary bg-primary/10 rounded-lg hover:bg-primary/20 transition-all"
>
<DashboardIcon name="plus" size="4" />
Добавить сотрудника
</button>
</div>
<!-- Workers Table -->
<div class="border border-layer-line rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-line-2">
<thead class="bg-muted/30">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
ФИО
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
Должность
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
Почта
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
Телефон
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
Кабинет
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
Действия
</th>
</tr>
</thead>
<tbody class="divide-y divide-line-2 bg-layer">
<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">
<span
class="text-primary hover:underline cursor-pointer"
title="Открыть на сайте"
@click="openPerson(worker.slug)"
>{{ worker.name }}</span>
</td>
<td class="px-4 py-3 text-sm text-foreground">
{{ worker.pivot.position }}
</td>
<td class="px-4 py-3 text-sm text-foreground">
{{ worker.pivot.service_email || '—' }}
</td>
<td class="px-4 py-3 text-sm text-foreground">
{{ worker.pivot.service_phone || '—' }}
</td>
<td class="px-4 py-3 text-sm text-foreground">
{{ worker.pivot.cabinet || '—' }}
</td>
<td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-1">
<button
type="button"
@click="editWorker(worker)"
class="p-1.5 text-muted-foreground-1 hover:text-primary hover:bg-primary/10 rounded transition-all"
title="Редактировать"
>
<DashboardIcon name="pencil-square" size="4" />
</button>
<button
type="button"
@click="confirmDetach(worker)"
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all"
title="Удалить"
>
<DashboardIcon name="trash" size="4" />
</button>
</div>
</td>
</tr>
<!-- Empty State -->
<tr v-if="workers.data.length === 0">
<td colspan="6" class="px-4 py-12 text-center">
<DashboardIcon name="user-group" size="12" class="mx-auto text-muted-foreground-1" />
<p class="mt-2 text-sm text-muted-foreground-1">Нет сотрудников</p>
<p class="text-xs text-muted-foreground-1">Добавьте первого сотрудника</p>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Add/Edit Modal -->
<div
v-if="showAddModal || editingWorker"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
@click.self="closeModal"
>
<div class="bg-layer border border-layer-line rounded-lg shadow-xl max-w-lg w-full">
<div class="px-6 py-4 border-b border-layer-line flex items-center justify-between">
<h3 class="text-lg font-medium text-foreground">
{{ editingWorker ? 'Редактировать сотрудника' : 'Добавить сотрудника' }}
</h3>
<button
type="button"
@click="closeModal"
class="p-1.5 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded transition-all"
>
<DashboardIcon name="x-mark" size="5" />
</button>
</div>
<form @submit.prevent="submit" class="p-6 space-y-4">
<div v-if="!editingWorker">
<label class="block text-sm font-medium text-foreground mb-1">
Сотрудник <span class="text-danger">*</span>
</label>
<select
v-model="form.user_id"
required
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option :value="null">Выберите сотрудника...</option>
<option v-for="user in availableUsers" :key="user.id" :value="user.id">
{{ user.name }}
</option>
</select>
<p v-if="$page.props.errors.user_id" class="mt-1 text-sm text-danger">{{ $page.props.errors.user_id }}</p>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">
Должность <span class="text-danger">*</span>
</label>
<input
v-model="form.position"
type="text"
required
maxlength="255"
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"
/>
<p v-if="$page.props.errors.position" class="mt-1 text-sm text-danger">{{ $page.props.errors.position }}</p>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-foreground mb-1">
Рабочая почта
</label>
<input
v-model="form.service_email"
type="email"
maxlength="255"
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"
/>
<p v-if="$page.props.errors.service_email" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_email }}</p>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">
Рабочий телефон
</label>
<input
v-model="form.service_phone"
type="text"
maxlength="20"
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"
/>
<p v-if="$page.props.errors.service_phone" class="mt-1 text-sm text-danger">{{ $page.props.errors.service_phone }}</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">
Кабинет
</label>
<input
v-model="form.cabinet"
type="text"
maxlength="10"
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"
/>
<p v-if="$page.props.errors.cabinet" class="mt-1 text-sm text-danger">{{ $page.props.errors.cabinet }}</p>
</div>
<div class="flex items-center justify-end gap-3 pt-4">
<button
type="button"
@click="closeModal"
class="px-4 py-2 text-sm font-medium text-foreground bg-surface border border-layer-line rounded-lg hover:bg-muted-hover transition-all"
>
Отмена
</button>
<button
type="submit"
:disabled="processing"
class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ processing ? 'Сохранение...' : 'Сохранить' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import DashboardIcon from '../../Components/DashboardIcon.vue';
import { Link } from '@inertiajs/vue3';
export default {
name: 'DepartmentWorkers',
components: {
DashboardIcon,
Link,
},
props: {
department: { type: Object, required: true },
workers: { type: Object, required: true },
availableUsers: { type: Array, required: true },
},
data() {
return {
showAddModal: false,
editingWorker: null,
processing: false,
form: {
user_id: null,
position: '',
service_email: '',
service_phone: '',
cabinet: ''
}
};
},
methods: {
editWorker(worker) {
this.editingWorker = worker;
this.form = {
user_id: worker.id,
position: worker.pivot.position,
service_email: worker.pivot.service_email || '',
service_phone: worker.pivot.service_phone || '',
cabinet: worker.pivot.cabinet || ''
};
},
confirmDetach(worker) {
if (confirm(`Вы уверены, что хотите удалить "${worker.name}" из сотрудников кафедры?`)) {
this.detachWorker(worker);
}
},
detachWorker(worker) {
this.$inertia.delete(route('dashboard.departments.workers.detach', [this.department.id, worker.id]), {
preserveScroll: true,
onSuccess: () => {
// Остаемся на странице edit, flash message покажется автоматически
}
});
},
submit() {
this.processing = true;
const url = this.editingWorker
? route('dashboard.departments.workers.update', [this.department.id, this.editingWorker.id])
: route('dashboard.departments.workers.attach', this.department.id);
const method = this.editingWorker ? 'put' : 'post';
this.$inertia[method](url, this.form, {
preserveScroll: true,
onSuccess: () => {
this.processing = false;
this.closeModal();
},
onError: () => {
this.processing = false;
}
});
},
closeModal() {
this.showAddModal = false;
this.editingWorker = null;
this.form = {
user_id: null,
position: '',
service_email: '',
service_phone: '',
cabinet: ''
};
this.$page.props.errors = {};
},
openPerson(slug) {
window.open(route('client.person.show', slug), '_blank');
}
}
}
</script>