Files
ntspi-app/resources/js/Pages/Dashboard/Schedules/Create.vue
T
2026-04-07 17:54:26 +05:00

316 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="min-h-screen bg-background-2">
<!-- Header -->
<div class="border-b border-line-2 bg-layer/50 backdrop-blur-sm sticky top-0 z-10 h-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
<div class="flex items-center h-full gap-3">
<a
:href="route('dashboard.schedules.index')"
class="p-2 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
>
<DashboardIcon name="arrow-left" size="5" />
</a>
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
<DashboardIcon name="plus" size="5" class="text-primary" />
</div>
<div>
<h1 class="text-lg font-medium text-foreground">Добавление расписания</h1>
<p class="text-xs text-muted-foreground-1">Загрузите файл расписания для учебной группы</p>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Flash Messages -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="$page.props.flash?.error" class="mb-4 p-4 bg-rose-500/10 border border-rose-500/20 rounded-lg">
<div class="flex items-start gap-3">
<DashboardIcon name="exclamation-circle" size="5" class="text-rose-600 flex-shrink-0 mt-0.5" />
<span class="text-sm text-foreground font-medium">{{ $page.props.flash.error }}</span>
</div>
</div>
</transition>
<!-- Form Card -->
<div class="bg-layer border border-layer-line rounded-lg shadow-xs">
<div class="p-6 border-b border-line-2">
<div class="flex items-center gap-2">
<DashboardIcon name="document" size="5" class="text-primary" />
<h2 class="text-base font-medium text-foreground">Основная информация</h2>
</div>
<p class="text-xs text-muted-foreground-1 mt-1">
Заполните данные о расписании
</p>
</div>
<form @submit.prevent="submit" class="p-6">
<div class="space-y-6">
<!-- Educational Group -->
<div>
<label for="educational_group_id" class="block text-sm font-medium text-foreground mb-2">
Учебная группа <span class="text-rose-500">*</span>
</label>
<select
id="educational_group_id"
v-model="form.educational_group_id"
:class="[
'w-full px-4 py-2.5 bg-surface border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all',
errors.educational_group_id ? 'border-rose-500' : 'border-layer-line focus:border-primary'
]"
>
<option value="">Выберите группу</option>
<option v-for="group in educationalGroups" :key="group.id" :value="group.id">
{{ group.title }}
</option>
</select>
<p v-if="errors.educational_group_id" class="mt-1.5 text-xs text-rose-500">{{ errors.educational_group_id }}</p>
</div>
<!-- File Upload -->
<div>
<label class="block text-sm font-medium text-foreground mb-2">
Файл расписания <span class="text-rose-500">*</span>
</label>
<div class="space-y-3">
<!-- Title Input -->
<input
v-model="form.file[0].title"
type="text"
placeholder="Название файла (например: Расписание на весенний семестр 2024)"
:class="[
'w-full px-4 py-2.5 bg-surface border rounded-lg text-sm text-foreground placeholder-muted-foreground-2 focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all',
errors['file.0.title'] ? 'border-rose-500' : 'border-layer-line focus:border-primary'
]"
/>
<p v-if="errors['file.0.title']" class="text-xs text-rose-500">{{ errors['file.0.title'] }}</p>
<!-- File Input -->
<div
class="relative border-2 border-dashed rounded-lg p-6 transition-all"
:class="[
isDragging ? 'border-primary bg-primary/5' : 'border-layer-line hover:border-primary/50',
errors['file.0.path'] ? 'border-rose-500' : ''
]"
@dragover.prevent="isDragging = true"
@dragleave="isDragging = false"
@drop.prevent="handleDrop"
>
<input
ref="fileInput"
type="file"
accept=".pdf,application/pdf"
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
:class="{ 'cursor-default': errors['file.0.path'] }"
@change="handleFileChange"
/>
<div class="text-center">
<DashboardIcon name="cloud-arrow-up" size="12" class="mx-auto text-muted-foreground-2" />
<p class="mt-2 text-sm text-foreground">
<span class="font-medium text-primary">Нажмите для загрузки</span> или перетащите файл
</p>
<p class="mt-1 text-xs text-muted-foreground-1">
Только PDF файлы, макс. размер 10MB
</p>
</div>
</div>
<!-- Selected File Preview -->
<div v-if="selectedFile" class="flex items-center gap-3 p-3 bg-surface border border-layer-line rounded-lg">
<div class="w-10 h-10 flex-shrink-0 rounded-lg bg-rose-500/10 flex items-center justify-center">
<DashboardIcon name="document" size="5" class="text-rose-500" />
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-foreground truncate">{{ selectedFile.name }}</p>
<p class="text-xs text-muted-foreground-1">{{ formatFileSize(selectedFile.size) }}</p>
</div>
<button
type="button"
@click="removeFile"
class="p-2 text-muted-foreground-1 hover:text-rose-500 hover:bg-rose-500/10 rounded-lg transition-all"
>
<DashboardIcon name="x-mark" size="4" />
</button>
</div>
<p v-if="errors['file.0.path']" class="text-xs text-rose-500">{{ errors['file.0.path'] }}</p>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-line-2">
<a
:href="route('dashboard.schedules.index')"
class="inline-flex items-center gap-2 px-4 py-2.5 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
>
Отмена
</a>
<button
type="submit"
:disabled="processing || !selectedFile"
class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg v-if="processing" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<DashboardIcon v-else name="check" size="4" />
{{ processing ? 'Сохранение...' : 'Создать расписание' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import DashboardIcon from '../Components/DashboardIcon.vue';
export default {
name: 'ScheduleCreate',
components: {
DashboardIcon,
},
props: {
educationalGroups: {
type: Array,
required: true
},
educationForms: {
type: Array,
required: true
}
},
data() {
return {
form: {
educational_group_id: '',
file: [
{
title: '',
path: null
}
]
},
selectedFile: null,
isDragging: false,
errors: {},
processing: false
}
},
mounted() {
this.SET_DOCUMENT_TITLE('Создание расписания');
},
watch: {
'form.file[0].path'(newFile) {
if (newFile && !this.selectedFile) {
this.selectedFile = newFile;
}
}
},
methods: {
handleFileChange(event) {
const file = event.target.files[0];
if (file) {
this.validateFile(file);
}
},
handleDrop(event) {
this.isDragging = false;
const file = event.dataTransfer.files[0];
if (file) {
this.validateFile(file);
}
},
validateFile(file) {
// Проверка типа файла
if (file.type !== 'application/pdf') {
this.errors['file.0.path'] = 'Разрешены только PDF файлы';
return;
}
// Проверка размера (10MB)
if (file.size > 10 * 1024 * 1024) {
this.errors['file.0.path'] = 'Размер файла не должен превышать 10MB';
return;
}
this.errors['file.0.path'] = '';
this.selectedFile = file;
this.form.file[0].path = file;
// Автозаполнение названия если пустое
if (!this.form.file[0].title) {
this.form.file[0].title = file.name.replace('.pdf', '');
}
},
removeFile() {
this.selectedFile = null;
this.form.file[0].path = null;
this.$refs.fileInput.value = '';
},
formatFileSize(bytes) {
if (!bytes) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
},
submit() {
if (!this.form.educational_group_id) {
this.errors.educational_group_id = 'Необходимо выбрать учебную группу';
return;
}
if (!this.form.file[0].path) {
this.errors['file.0.path'] = 'Необходимо загрузить файл';
return;
}
this.processing = true;
this.errors = {};
const formData = new FormData();
formData.append('educational_group_id', this.form.educational_group_id);
formData.append('file[0][title]', this.form.file[0].title);
formData.append('file[0][path]', this.form.file[0].path);
this.$inertia.post(route('dashboard.schedules.store'), formData, {
onFinish: () => {
this.processing = false;
},
onError: (errors) => {
this.errors = errors;
}
});
},
route(name, params) {
return route(name, params);
}
}
}
</script>