AdminLTE3
This commit is contained in:
344
views/admin/services/add.ejs
Normal file
344
views/admin/services/add.ejs
Normal file
@@ -0,0 +1,344 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-plus mr-2"></i>Добавить услугу</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/services">Услуги</a></li>
|
||||
<li class="breadcrumb-item active">Добавить</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<form id="serviceForm" action="/api/admin/services" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Basic Information -->
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Основная информация</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name">Название услуги *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="shortDescription">Краткое описание</label>
|
||||
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2" placeholder="Краткое описание для списков и карточек"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Полное описание</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6" placeholder="Детальное описание услуги"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="category">Категория *</label>
|
||||
<select class="form-control" id="category" name="category" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
<option value="web-development">Веб-разработка</option>
|
||||
<option value="mobile-development">Мобильная разработка</option>
|
||||
<option value="ui-ux-design">UI/UX Дизайн</option>
|
||||
<option value="consulting">Консалтинг</option>
|
||||
<option value="support">Поддержка</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="icon">Иконка</label>
|
||||
<input type="text" class="form-control" id="icon" name="icon" placeholder="fas fa-code" value="fas fa-cog">
|
||||
<small class="form-text text-muted">FontAwesome класс иконки</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="estimatedTime">Время выполнения</label>
|
||||
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" placeholder="2-4 недели">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="card card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Ценообразование</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="basePrice">Базовая цена ($)</label>
|
||||
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select class="form-control" id="currency" name="pricing[currency]">
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="KRW">KRW (₩)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pricingType">Тип ценообразования</label>
|
||||
<select class="form-control" id="pricingType" name="pricing[type]">
|
||||
<option value="fixed">Фиксированная цена</option>
|
||||
<option value="hourly">Почасовая оплата</option>
|
||||
<option value="project">За проект</option>
|
||||
<option value="subscription">Подписка</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Функции и возможности</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="featuresContainer">
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[0][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
|
||||
<i class="fas fa-plus mr-1"></i> Добавить функцию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Status & Settings -->
|
||||
<div class="card card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" checked>
|
||||
<label class="custom-control-label" for="isActive">Активная услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="featured" name="featured">
|
||||
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="order">Порядок отображения</label>
|
||||
<input type="number" class="form-control" id="order" name="order" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Теги</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="tags">Теги (через запятую)</label>
|
||||
<input type="text" class="form-control" id="tags" name="tags" placeholder="веб-дизайн, frontend, react">
|
||||
<small class="form-text text-muted">Разделите теги запятыми</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO -->
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SEO настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="seoTitle">SEO заголовок</label>
|
||||
<input type="text" class="form-control" id="seoTitle" name="seo[title]">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoDescription">SEO описание</label>
|
||||
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoKeywords">Ключевые слова</label>
|
||||
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" placeholder="через, запятую">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save mr-1"></i> Сохранить услугу
|
||||
</button>
|
||||
<a href="/admin/services" class="btn btn-secondary ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let featureIndex = 1;
|
||||
|
||||
// Add feature
|
||||
$('#addFeature').click(function() {
|
||||
const featureHtml = `
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[${featureIndex}][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#featuresContainer').append(featureHtml);
|
||||
featureIndex++;
|
||||
});
|
||||
|
||||
// Remove feature
|
||||
$(document).on('click', '.remove-feature', function() {
|
||||
$(this).closest('.feature-item').remove();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#serviceForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = {};
|
||||
|
||||
// Convert FormData to object
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// Handle nested objects (pricing, seo, features)
|
||||
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
|
||||
if (matches) {
|
||||
const [, parent, child, grandchild] = matches;
|
||||
if (!data[parent]) data[parent] = {};
|
||||
|
||||
if (grandchild) {
|
||||
// features array handling
|
||||
if (!data[parent][child]) data[parent][child] = {};
|
||||
data[parent][child][grandchild] = value;
|
||||
} else {
|
||||
data[parent][child] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert features object to array
|
||||
if (data.features) {
|
||||
const featuresArray = [];
|
||||
Object.keys(data.features).forEach(index => {
|
||||
if (data.features[index].name) {
|
||||
featuresArray.push({
|
||||
name: data.features[index].name,
|
||||
included: data.features[index].included === 'true'
|
||||
});
|
||||
}
|
||||
});
|
||||
data.features = featuresArray;
|
||||
}
|
||||
|
||||
// Convert checkboxes
|
||||
data.isActive = $('#isActive').is(':checked');
|
||||
data.featured = $('#featured').is(':checked');
|
||||
|
||||
// Convert tags to array
|
||||
if (data.tags) {
|
||||
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
}
|
||||
|
||||
fetch('/api/admin/services', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
toastr.success('Услуга успешно создана!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/services';
|
||||
}, 1500);
|
||||
} else {
|
||||
toastr.error(result.message || 'Ошибка при создании услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
toastr.error('Произошла ошибка при создании услуги');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
411
views/admin/services/edit.ejs
Normal file
411
views/admin/services/edit.ejs
Normal file
@@ -0,0 +1,411 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-edit mr-2"></i>Редактировать услугу</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/services">Услуги</a></li>
|
||||
<li class="breadcrumb-item active">Редактировать</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<form id="serviceForm">
|
||||
<input type="hidden" id="serviceId" value="<%= service.id %>">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Basic Information -->
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Основная информация</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name">Название услуги *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="shortDescription">Краткое описание</label>
|
||||
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Полное описание</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="category">Категория *</label>
|
||||
<select class="form-control" id="category" name="category" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
|
||||
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
|
||||
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
|
||||
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
|
||||
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
|
||||
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="icon">Иконка</label>
|
||||
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
|
||||
<small class="form-text text-muted">FontAwesome класс иконки</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="estimatedTime">Время выполнения</label>
|
||||
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="card card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Ценообразование</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="basePrice">Базовая цена ($)</label>
|
||||
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select class="form-control" id="currency" name="pricing[currency]">
|
||||
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
|
||||
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
|
||||
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pricingType">Тип ценообразования</label>
|
||||
<select class="form-control" id="pricingType" name="pricing[type]">
|
||||
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
|
||||
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
|
||||
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
|
||||
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Функции и возможности</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="featuresContainer">
|
||||
<% if (service.features && service.features.length > 0) { %>
|
||||
<% service.features.forEach((feature, index) => { %>
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[<%= index %>][included]">
|
||||
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
|
||||
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[0][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
|
||||
<i class="fas fa-plus mr-1"></i> Добавить функцию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Status & Settings -->
|
||||
<div class="card card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
|
||||
<label class="custom-control-label" for="isActive">Активная услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
|
||||
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="order">Порядок отображения</label>
|
||||
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Теги</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="tags">Теги (через запятую)</label>
|
||||
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
|
||||
<small class="form-text text-muted">Разделите теги запятыми</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO -->
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SEO настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="seoTitle">SEO заголовок</label>
|
||||
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoDescription">SEO описание</label>
|
||||
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoKeywords">Ключевые слова</label>
|
||||
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save mr-1"></i> Сохранить изменения
|
||||
</button>
|
||||
<a href="/admin/services" class="btn btn-secondary ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Отмена
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
|
||||
<i class="fas fa-trash mr-1"></i> Удалить услугу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const service = <%- JSON.stringify(service) %>;
|
||||
let featureIndex = service.features ? service.features.length : 1;
|
||||
|
||||
// Add feature
|
||||
$('#addFeature').click(function() {
|
||||
const featureHtml = `
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[${featureIndex}][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#featuresContainer').append(featureHtml);
|
||||
featureIndex++;
|
||||
});
|
||||
|
||||
// Remove feature
|
||||
$(document).on('click', '.remove-feature', function() {
|
||||
$(this).closest('.feature-item').remove();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#serviceForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const serviceId = $('#serviceId').val();
|
||||
const formData = new FormData(this);
|
||||
const data = {};
|
||||
|
||||
// Convert FormData to object
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// Handle nested objects (pricing, seo, features)
|
||||
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
|
||||
if (matches) {
|
||||
const [, parent, child, grandchild] = matches;
|
||||
if (!data[parent]) data[parent] = {};
|
||||
|
||||
if (grandchild) {
|
||||
// features array handling
|
||||
if (!data[parent][child]) data[parent][child] = {};
|
||||
data[parent][child][grandchild] = value;
|
||||
} else {
|
||||
data[parent][child] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert features object to array
|
||||
if (data.features) {
|
||||
const featuresArray = [];
|
||||
Object.keys(data.features).forEach(index => {
|
||||
if (data.features[index].name) {
|
||||
featuresArray.push({
|
||||
name: data.features[index].name,
|
||||
included: data.features[index].included === 'true'
|
||||
});
|
||||
}
|
||||
});
|
||||
data.features = featuresArray;
|
||||
}
|
||||
|
||||
// Convert checkboxes
|
||||
data.isActive = $('#isActive').is(':checked');
|
||||
data.featured = $('#featured').is(':checked');
|
||||
|
||||
// Convert tags to array
|
||||
if (data.tags) {
|
||||
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
}
|
||||
|
||||
fetch(`/api/admin/services/${serviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
toastr.success('Услуга успешно обновлена!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/services';
|
||||
}, 1500);
|
||||
} else {
|
||||
toastr.error(result.message || 'Ошибка при обновлении услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
toastr.error('Произошла ошибка при обновлении услуги');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function deleteService() {
|
||||
const serviceId = $('#serviceId').val();
|
||||
|
||||
Swal.fire({
|
||||
title: 'Удалить услугу?',
|
||||
text: 'Это действие невозможно отменить!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Да, удалить!',
|
||||
cancelButtonText: 'Отмена'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/api/admin/services/${serviceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
|
||||
window.location.href = '/admin/services';
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,121 +1,206 @@
|
||||
<!-- Services List -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-cog mr-2"></i>
|
||||
Управление услугами
|
||||
</h3>
|
||||
<a href="/admin/services/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% if (services && services.length > 0) { %>
|
||||
<% services.forEach(service => { %>
|
||||
<li>
|
||||
<div class="px-4 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<i class="<%= service.icon || 'fas fa-cog' %> text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm font-medium text-gray-900"><%= service.name %></div>
|
||||
<% if (service.featured) { %>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Рекомендуемая
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (!service.isActive) { %>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Неактивна
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= service.category %> •
|
||||
<% if (service.pricing && service.pricing.basePrice) { %>
|
||||
от $<%= service.pricing.basePrice %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="/services#<%= service.id %>" target="_blank" class="text-blue-600 hover:text-blue-900">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<a href="/admin/services/edit/<%= service.id %>" class="text-indigo-600 hover:text-indigo-900">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteService('<%= service.id %>')" class="text-red-600 hover:text-red-900">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-cog text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">Услуги не найдены</p>
|
||||
<a href="/admin/services/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
Добавить первую услугу
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Предыдущая
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-cogs mr-2"></i>Управление услугами</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="float-sm-right">
|
||||
<a href="/admin/services/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (pagination.hasNext) { %>
|
||||
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Следующая
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Список услуг</h3>
|
||||
<div class="card-tools">
|
||||
<div class="input-group input-group-sm" style="width: 150px;">
|
||||
<input type="text" name="table_search" class="form-control float-right" placeholder="Поиск">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-default">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body table-responsive p-0">
|
||||
<% if (services && services.length > 0) { %>
|
||||
<table class="table table-hover text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Категория</th>
|
||||
<th>Статус</th>
|
||||
<th>Цена от</th>
|
||||
<th>Время</th>
|
||||
<th>Рекомендуемая</th>
|
||||
<th>Создано</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(service => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<small class="text-muted">#<%= service.id.slice(-8) %></small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="<%= service.icon || 'fas fa-cog' %> mr-2 text-primary"></i>
|
||||
<strong><%= service.name %></strong>
|
||||
</div>
|
||||
<% if (service.shortDescription) { %>
|
||||
<small class="text-muted d-block">
|
||||
<%= service.shortDescription.substring(0, 80) %><%= service.shortDescription.length > 80 ? '...' : '' %>
|
||||
</small>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info"><%= service.category %></span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.isActive) { %>
|
||||
<span class="badge badge-success">Активна</span>
|
||||
<% } else { %>
|
||||
<span class="badge badge-secondary">Неактивна</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.pricing && service.pricing.basePrice) { %>
|
||||
<span class="text-success font-weight-bold">$<%= service.pricing.basePrice %></span>
|
||||
<% } else { %>
|
||||
<span class="text-muted">Не указана</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.estimatedTime) { %>
|
||||
<i class="fas fa-clock mr-1 text-info"></i>
|
||||
<%= service.estimatedTime %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.featured) { %>
|
||||
<i class="fas fa-star text-warning" title="Рекомендуемая услуга"></i>
|
||||
<% } else { %>
|
||||
<i class="far fa-star text-muted"></i>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= new Date(service.createdAt).toLocaleDateString('ru-RU') %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/services#<%= service.id %>" target="_blank" class="btn btn-info btn-sm" title="Посмотреть на сайте">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="/admin/services/edit/<%= service.id %>" class="btn btn-warning btn-sm" title="Редактировать">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteService('<%= service.id %>')" class="btn btn-danger btn-sm" title="Удалить">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } else { %>
|
||||
<div class="p-4 text-center">
|
||||
<i class="fas fa-cogs text-muted" style="font-size: 4rem;"></i>
|
||||
<h4 class="mt-3 text-muted">Услуги не найдены</h4>
|
||||
<p class="text-muted">Добавьте первую услугу для начала работы</p>
|
||||
<a href="/admin/services/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="card-footer clearfix">
|
||||
<ul class="pagination pagination-sm m-0 float-right">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %>">«</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<li class="page-item <%= i === pagination.current ? 'active' : '' %>">
|
||||
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.hasNext) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %>">»</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function deleteService(id) {
|
||||
if (confirm('Вы уверены, что хотите удалить эту услугу?')) {
|
||||
fetch(`/api/admin/services/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при удалении услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении услуги');
|
||||
});
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Удалить услугу?',
|
||||
text: 'Это действие невозможно отменить!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Да, удалить!',
|
||||
cancelButtonText: 'Отмена'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/api/admin/services/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user