258 lines
7.1 KiB
JavaScript
258 lines
7.1 KiB
JavaScript
const { Service, Category } = require('../models');
|
|
const CacheService = require('./cacheService');
|
|
const logger = require('../config/logger');
|
|
const { Op } = require('sequelize');
|
|
|
|
/**
|
|
* Admin Service Management Business Logic
|
|
*/
|
|
class AdminServiceService {
|
|
/**
|
|
* Get all services with pagination and filtering (admin view)
|
|
* @param {Object} options - Query options
|
|
* @returns {Promise<Object>} - Paginated services
|
|
*/
|
|
static async getServices({ page = 1, limit = 10, categoryId, status, search }) {
|
|
try {
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Build query
|
|
const where = {};
|
|
if (categoryId) {
|
|
where.categoryId = categoryId;
|
|
}
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
if (search) {
|
|
where[Op.or] = [
|
|
{ titleZh: { [Op.like]: `%${search}%` } },
|
|
{ titleEn: { [Op.like]: `%${search}%` } },
|
|
{ titleEs: { [Op.like]: `%${search}%` } },
|
|
];
|
|
}
|
|
|
|
// Fetch from database
|
|
const { count, rows } = await Service.findAndCountAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: Category,
|
|
as: 'category',
|
|
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
|
},
|
|
],
|
|
order: [['sortOrder', 'ASC'], ['createdAt', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
});
|
|
|
|
return {
|
|
data: rows.map(service => service.toJSON()),
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total: count,
|
|
totalPages: Math.ceil(count / limit),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error fetching services (admin):', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new service
|
|
* @param {Object} serviceData - Service data
|
|
* @returns {Promise<Object>} - Created service
|
|
*/
|
|
static async createService(serviceData) {
|
|
try {
|
|
// Validate category exists
|
|
const category = await Category.findByPk(serviceData.categoryId);
|
|
if (!category) {
|
|
const error = new Error('Category not found');
|
|
error.statusCode = 404;
|
|
error.code = 'CATEGORY_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
|
|
// Validate required fields
|
|
const requiredFields = ['categoryId', 'titleZh', 'titleEn', 'titleEs'];
|
|
for (const field of requiredFields) {
|
|
if (!serviceData[field]) {
|
|
const error = new Error(`Missing required field: ${field}`);
|
|
error.statusCode = 400;
|
|
error.code = 'MISSING_REQUIRED_FIELD';
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Create service
|
|
const service = await Service.create({
|
|
categoryId: serviceData.categoryId,
|
|
serviceType: serviceData.serviceType || null,
|
|
titleZh: serviceData.titleZh,
|
|
titleEn: serviceData.titleEn,
|
|
titleEs: serviceData.titleEs,
|
|
descriptionZh: serviceData.descriptionZh || null,
|
|
descriptionEn: serviceData.descriptionEn || null,
|
|
descriptionEs: serviceData.descriptionEs || null,
|
|
image: serviceData.image || null,
|
|
price: serviceData.price || null,
|
|
status: serviceData.status || 'active',
|
|
sortOrder: serviceData.sortOrder || 0,
|
|
});
|
|
|
|
// Invalidate cache
|
|
await CacheService.invalidateServiceCache();
|
|
|
|
logger.info(`Service created: ${service.id}`);
|
|
return service.toJSON();
|
|
} catch (error) {
|
|
logger.error('Error creating service:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a service
|
|
* @param {string} serviceId - Service ID
|
|
* @param {Object} updateData - Update data
|
|
* @returns {Promise<Object>} - Updated service
|
|
*/
|
|
static async updateService(serviceId, updateData) {
|
|
try {
|
|
// Find service
|
|
const service = await Service.findByPk(serviceId);
|
|
if (!service) {
|
|
const error = new Error('Service not found');
|
|
error.statusCode = 404;
|
|
error.code = 'SERVICE_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
|
|
// Validate category if being updated
|
|
if (updateData.categoryId) {
|
|
const category = await Category.findByPk(updateData.categoryId);
|
|
if (!category) {
|
|
const error = new Error('Category not found');
|
|
error.statusCode = 404;
|
|
error.code = 'CATEGORY_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update service
|
|
const allowedFields = [
|
|
'categoryId',
|
|
'serviceType',
|
|
'titleZh',
|
|
'titleEn',
|
|
'titleEs',
|
|
'descriptionZh',
|
|
'descriptionEn',
|
|
'descriptionEs',
|
|
'image',
|
|
'price',
|
|
'status',
|
|
'sortOrder',
|
|
];
|
|
|
|
const updateFields = {};
|
|
for (const field of allowedFields) {
|
|
if (updateData[field] !== undefined) {
|
|
updateFields[field] = updateData[field];
|
|
}
|
|
}
|
|
|
|
await service.update(updateFields);
|
|
|
|
// Invalidate cache
|
|
await CacheService.invalidateServiceCache();
|
|
await CacheService.invalidateServiceDetail(serviceId);
|
|
|
|
logger.info(`Service updated: ${serviceId}`);
|
|
return service.toJSON();
|
|
} catch (error) {
|
|
logger.error(`Error updating service ${serviceId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a service
|
|
* @param {string} serviceId - Service ID
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
static async deleteService(serviceId) {
|
|
try {
|
|
// Find service
|
|
const service = await Service.findByPk(serviceId);
|
|
if (!service) {
|
|
const error = new Error('Service not found');
|
|
error.statusCode = 404;
|
|
error.code = 'SERVICE_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
|
|
// Check if service has appointments
|
|
const { Appointment } = require('../models');
|
|
const appointmentCount = await Appointment.count({
|
|
where: { serviceId },
|
|
});
|
|
|
|
if (appointmentCount > 0) {
|
|
const error = new Error('Cannot delete service with existing appointments');
|
|
error.statusCode = 400;
|
|
error.code = 'SERVICE_HAS_APPOINTMENTS';
|
|
throw error;
|
|
}
|
|
|
|
// Delete service
|
|
await service.destroy();
|
|
|
|
// Invalidate cache
|
|
await CacheService.invalidateServiceCache();
|
|
await CacheService.invalidateServiceDetail(serviceId);
|
|
|
|
logger.info(`Service deleted: ${serviceId}`);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`Error deleting service ${serviceId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get service by ID (admin view with all fields)
|
|
* @param {string} serviceId - Service ID
|
|
* @returns {Promise<Object>} - Service details
|
|
*/
|
|
static async getServiceById(serviceId) {
|
|
try {
|
|
const service = await Service.findByPk(serviceId, {
|
|
include: [
|
|
{
|
|
model: Category,
|
|
as: 'category',
|
|
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!service) {
|
|
return null;
|
|
}
|
|
|
|
return service.toJSON();
|
|
} catch (error) {
|
|
logger.error(`Error fetching service ${serviceId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AdminServiceService;
|