"use server" import { redirect } from "next/navigation" import { mockDb } from '@/lib/mockDb' import { uploadToOpeninary } from '@/lib/openinary' import { requireAdmin } from '@/lib/auth' import { HONEYPOT_FIELD_NAME } from '@/components/HoneypotField' import { checkRateLimit } from '@/lib/rateLimit' import { sendTelegramMessage, formatContactMessageNotification, formatBusinessSubmissionNotification } from '@/lib/telegram' import { headers } from 'next/headers' import { revalidatePath } from 'next/cache' async function getClientIp() { const h = await headers() return h.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown' } export async function submitBusinessAction(formData: FormData) { if (formData.get(HONEYPOT_FIELD_NAME)) { // Bot trap tripped — pretend success so it doesn't learn to skip this field. return { success: true, error: undefined } } const ip = await getClientIp() if (!checkRateLimit(`submit-business:${ip}`, 5, 10 * 60_000)) { return { error: 'Çok fazla başvuru gönderildi. Lütfen daha sonra tekrar deneyin.' } } const businessName = formData.get('businessName') as string const categoryId = formData.get('categoryId') as string const neighborhoodId = formData.get('neighborhoodId') as string const address = formData.get('address') as string const phone = formData.get('phone') as string const whatsapp = formData.get('whatsapp') as string const description = formData.get('description') as string const contactName = formData.get('contactName') as string const contactEmail = formData.get('contactEmail') as string const imageFile = formData.get('imageFile') as File | null if (!businessName || !categoryId || !neighborhoodId || !address || !description || !contactName || !contactEmail) { return { error: 'Lütfen tüm zorunlu alanları doldurun.' } } let imageUrl = null if (imageFile && imageFile.size > 0) { try { imageUrl = await uploadToOpeninary(imageFile, 'submissions') } catch (e: any) { console.error('Openinary upload error:', e) return { error: `Görsel yüklenemedi: ${e.message}` } } } await mockDb.createSubmission({ businessName, categoryId, neighborhoodId, address, phone, whatsapp, description, contactName, contactEmail, imageUrl }) const [categories, neighborhoods] = await Promise.all([mockDb.getCategories(), mockDb.getNeighborhoods()]) await sendTelegramMessage( formatBusinessSubmissionNotification({ businessName, categoryName: categories.find(c => c.id === categoryId)?.nameTr || categoryId, neighborhoodName: neighborhoods.find(n => n.id === neighborhoodId)?.nameTr || neighborhoodId, address, phone, whatsapp, description, contactName, contactEmail, ip, }) ) revalidatePath('/admin/submissions') return { success: true, error: undefined } } export async function submitContactMessageAction(formData: FormData) { if (formData.get(HONEYPOT_FIELD_NAME)) { return { success: true, error: undefined } } const ip = await getClientIp() if (!checkRateLimit(`submit-contact:${ip}`, 5, 10 * 60_000)) { return { error: 'Çok fazla mesaj gönderildi. Lütfen daha sonra tekrar deneyin.' } } const name = formData.get('name') as string const email = formData.get('email') as string const subject = formData.get('subject') as string const message = formData.get('message') as string if (!name || !email || !subject || !message) { return { error: 'Lütfen tüm alanları doldurun.' } } await mockDb.createMessage({ name, email, subject, message }) await sendTelegramMessage(formatContactMessageNotification({ name, email, subject, message, ip })) revalidatePath('/admin/messages') return { success: true, error: undefined } } export async function approveSubmissionAction(id: string) { await requireAdmin() const submission = await mockDb.getSubmissionById(id) if (!submission) return { error: 'Başvuru bulunamadı' } const slug = submission.businessName.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') await mockDb.createListing({ slug, categoryId: submission.categoryId, neighborhoodId: submission.neighborhoodId, city: 'marmaris', nameTr: submission.businessName, nameEn: submission.businessName, nameRu: submission.businessName, descriptionTr: submission.description, descriptionEn: submission.description, descriptionRu: submission.description, address: submission.address, phone: submission.phone, whatsapp: submission.whatsapp, priceRange: 2, rating: 5.0, isLocalApproved: false, isFeatured: false, hasWidgetInstalled: false, images: submission.imageUrl ? [submission.imageUrl] : [] }) await mockDb.updateSubmissionStatus(id, 'APPROVED') revalidatePath('/admin/submissions') revalidatePath('/restaurants') revalidatePath('/aparts') revalidatePath('/businesses') return { success: true, error: undefined } } export async function rejectSubmissionAction(id: string) { await requireAdmin() await mockDb.updateSubmissionStatus(id, 'REJECTED') revalidatePath('/admin/submissions') return { success: true, error: undefined } } export async function deleteListingAction(id: string) { await requireAdmin() await mockDb.deleteListing(id) revalidatePath('/admin/listings') revalidatePath('/restaurants') revalidatePath('/aparts') revalidatePath('/businesses') return { success: true, error: undefined } } export async function restoreListingAction(id: string) { await requireAdmin() await mockDb.restoreListing(id) revalidatePath('/admin/trash') revalidatePath('/admin/listings') return { success: true, error: undefined } } export async function hardDeleteListingAction(id: string) { await requireAdmin() try { await mockDb.hardDeleteListing(id) revalidatePath('/admin/trash') return { success: true, error: undefined } } catch (err: any) { if (err.message?.includes('RESTRICT') || err.message?.includes('Foreign key constraint')) { return { success: false, error: 'Silme başarısız: Bu mekana bağlı etkinlik (Event) veya kayıtlar var.' } } return { success: false, error: err.message || 'Bilinmeyen bir hata oluştu.' } } } export async function markMessageReadAction(id: string) { await requireAdmin() await mockDb.markMessageAsRead(id) revalidatePath('/admin/messages') return { success: true, error: undefined } } export async function createOrUpdateListingAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const slug = formData.get('slug') as string const categoryId = formData.get('categoryId') as string const neighborhoodId = formData.get('neighborhoodId') as string const nameTr = formData.get('nameTr') as string const nameEn = formData.get('nameEn') as string const nameRu = formData.get('nameRu') as string const descriptionTr = formData.get('descriptionTr') as string const descriptionEn = formData.get('descriptionEn') as string const descriptionRu = formData.get('descriptionRu') as string const address = formData.get('address') as string if (!slug || !categoryId || !neighborhoodId || !nameTr || !address) { return { success: false, error: 'Lütfen zorunlu alanları (Slug, Kategori, Mahalle, İsim, Adres) doldurun.' } } const phone = formData.get('phone') as string || null const whatsapp = formData.get('whatsapp') as string || null const website = formData.get('website') as string || null const instagram = formData.get('instagram') as string || null const priceRange = parseInt(formData.get('priceRange') as string) const rating = formData.get('rating') ? parseFloat(formData.get('rating') as string) : null const isLocalApproved = formData.get('isLocalApproved') === 'true' const isFeatured = formData.get('isFeatured') === 'true' const latitude = formData.get('latitude') ? parseFloat(formData.get('latitude') as string) : null const longitude = formData.get('longitude') ? parseFloat(formData.get('longitude') as string) : null const openingHoursStr = formData.get('openingHours') as string const openingHours = openingHoursStr ? { all: openingHoursStr } : null // Handle images const images: string[] = [] const imageFile1 = formData.get('imageFile1') as File | null const imageUrl1 = formData.get('imageUrl1') as string | null const imageFile2 = formData.get('imageFile2') as File | null const imageUrl2 = formData.get('imageUrl2') as string | null // Process first image if (imageFile1 && imageFile1.size > 0) { try { const url = await uploadToOpeninary(imageFile1, `listings/${slug}`) images.push(url) } catch (e: any) { console.error('Image 1 upload error:', e) return { success: false, error: `1. Görsel yüklenemedi: ${e.message}` } } } else if (imageUrl1) { images.push(imageUrl1) } // Process second image if (imageFile2 && imageFile2.size > 0) { try { const url = await uploadToOpeninary(imageFile2, `listings/${slug}`) images.push(url) } catch (e: any) { console.error('Image 2 upload error:', e) return { success: false, error: `2. Görsel yüklenemedi: ${e.message}` } } } else if (imageUrl2) { images.push(imageUrl2) } const data = { slug, categoryId, neighborhoodId, city: 'marmaris', nameTr, nameEn, nameRu, descriptionTr, descriptionEn, descriptionRu, address, phone, whatsapp, website, instagram, priceRange, rating, isLocalApproved, isFeatured, latitude, longitude, openingHours, images, hasWidgetInstalled: false } try { if (id && id !== 'new') { await mockDb.updateListing(id, data) } else { await mockDb.createListing(data) } revalidatePath('/admin/listings') revalidatePath('/restaurants') revalidatePath('/aparts') revalidatePath('/businesses') return { success: true, error: undefined } } catch (err: any) { return { success: false, error: err.message || 'Mekan kaydedilemedi.' } } } // Blog Actions export async function deleteBlogPostAction(id: string) { await requireAdmin() await mockDb.deleteBlogPost(id) revalidatePath('/admin/blog') revalidatePath('/blog') return { success: true, error: undefined } } export async function createOrUpdateBlogPostAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const slug = formData.get('slug') as string const titleTr = formData.get('titleTr') as string const titleEn = formData.get('titleEn') as string const titleRu = formData.get('titleRu') as string const contentTr = formData.get('contentTr') as string const contentEn = formData.get('contentEn') as string const contentRu = formData.get('contentRu') as string if (!slug || !titleTr) { return { success: false, error: 'Lütfen zorunlu alanları (Slug, Başlık) doldurun.' } } const tagsStr = formData.get('tags') as string || '' const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean) const relatedListingIdsStr = formData.get('relatedListingIds') as string || '' const relatedListingIds = relatedListingIdsStr.split(',').map(i => i.trim()).filter(Boolean) const isPublished = formData.get('isPublished') === 'true' const publishedAt = isPublished ? new Date() : null // Handle Cover Image const coverImageFile = formData.get('coverImageFile') as File | null let coverImage = formData.get('coverImageUrl') as string | null || null if (coverImageFile && coverImageFile.size > 0) { try { coverImage = await uploadToOpeninary(coverImageFile, `blog/${slug}`) } catch (e: any) { console.error('Blog cover upload error:', e) return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` } } } const data = { slug, titleTr, titleEn, titleRu, contentTr, contentEn, contentRu, coverImage, tags, relatedListingIds, publishedAt } try { if (id && id !== 'new') { await mockDb.updateBlogPost(id, data) } else { await mockDb.createBlogPost(data) } revalidatePath('/admin/blog') revalidatePath('/blog') revalidatePath(`/blog/${slug}`) return { success: true, error: undefined } } catch (err: any) { return { success: false, error: err.message || 'Yazı kaydedilemedi.' } } } // Collections Actions export async function deleteCollectionAction(id: string) { await requireAdmin() await mockDb.deleteCollection(id) revalidatePath('/admin/collections') revalidatePath('/collections') return { success: true, error: undefined } } export async function createOrUpdateCollectionAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const slug = formData.get('slug') as string const titleTr = formData.get('titleTr') as string const titleEn = formData.get('titleEn') as string const titleRu = formData.get('titleRu') as string const descriptionTr = formData.get('descriptionTr') as string const descriptionEn = formData.get('descriptionEn') as string const descriptionRu = formData.get('descriptionRu') as string if (!slug || !titleTr) { return { success: false, error: 'Lütfen zorunlu alanları (Slug, Başlık) doldurun.' } } const listingIdsStr = formData.get('listingIds') as string || '' const listingIds = listingIdsStr.split(',').map(i => i.trim()).filter(Boolean) // Handle Cover Image const coverImageFile = formData.get('coverImageFile') as File | null let coverImage = formData.get('coverImageUrl') as string | null || null if (coverImageFile && coverImageFile.size > 0) { try { coverImage = await uploadToOpeninary(coverImageFile, `collections/${slug}`) } catch (e: any) { console.error('Collection cover upload error:', e) return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` } } } const data = { slug, titleTr, titleEn, titleRu, descriptionTr, descriptionEn, descriptionRu, coverImage, listingIds } try { if (id && id !== 'new') { await mockDb.updateCollection(id, data) } else { await mockDb.createCollection(data) } revalidatePath('/admin/collections') revalidatePath('/collections') revalidatePath(`/collection/${slug}`) return { success: true, error: undefined } } catch (err: any) { return { success: false, error: err.message || 'Koleksiyon kaydedilemedi.' } } } // Events Actions (Phase 3) export async function deleteEventAction(id: string) { await requireAdmin() await mockDb.deleteEvent(id) revalidatePath('/admin/events') revalidatePath('/events') return { success: true, error: undefined } } export async function createOrUpdateEventAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const slug = formData.get('slug') as string const listingId = formData.get('listingId') as string | null || null const titleTr = formData.get('titleTr') as string const titleEn = formData.get('titleEn') as string const titleRu = formData.get('titleRu') as string const descriptionTr = formData.get('descriptionTr') as string const descriptionEn = formData.get('descriptionEn') as string const descriptionRu = formData.get('descriptionRu') as string const startDateStr = formData.get('startDate') as string const endDateStr = formData.get('endDate') as string | null if (!slug || !titleTr || !startDateStr) { return { success: false, error: 'Lütfen zorunlu alanları (Slug, Başlık, Başlangıç Tarihi) doldurun.' } } const isSponsored = formData.get('isSponsored') === 'true' const startDate = new Date(startDateStr) const endDate = endDateStr ? new Date(endDateStr) : null // Handle Cover Image const coverImageFile = formData.get('coverImageFile') as File | null let coverImage = formData.get('coverImageUrl') as string | null || null if (coverImageFile && coverImageFile.size > 0) { try { coverImage = await uploadToOpeninary(coverImageFile, `events/${slug}`) } catch (e: any) { console.error('Event cover upload error:', e) return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` } } } const data = { slug, listingId, titleTr, titleEn, titleRu, descriptionTr, descriptionEn, descriptionRu, startDate, endDate, coverImage, isSponsored } try { if (id && id !== 'new') { await mockDb.updateEvent(id, data) } else { await mockDb.createEvent(data) } revalidatePath('/admin/events') revalidatePath('/events') revalidatePath(`/event/${slug}`) return { success: true, error: undefined } } catch (err: any) { return { success: false, error: err.message || 'Etkinlik kaydedilemedi.' } } } // Neighborhood Actions export async function deleteNeighborhoodAction(id: string) { await requireAdmin() try { const listings = await mockDb.getListings({ neighborhoodId: id }) if (listings && listings.length > 0) { return { success: false, error: `Silme başarısız: Bu mahalleye kayıtlı ${listings.length} adet mekan var.` } } await mockDb.deleteNeighborhood(id) revalidatePath('/admin/neighborhoods') return { success: true, error: undefined } } catch (err: any) { if (err.message?.includes('RESTRICT') || err.message?.includes('Foreign key constraint')) { return { success: false, error: 'Silme başarısız: Bu mahalleye bağlı gizli (silinmiş veya arşivlenmiş) mekanlar veya etkinlikler var.' } } return { success: false, error: err.message || 'Bilinmeyen bir hata oluştu.' } } } export async function createOrUpdateNeighborhoodAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const slug = formData.get('slug') as string const nameTr = formData.get('nameTr') as string const nameEn = formData.get('nameEn') as string const nameRu = formData.get('nameRu') as string if (!slug || !nameTr || !nameEn || !nameRu) { return { error: 'Zorunlu alanları doldurun.' } } const data = { slug, nameTr, nameEn, nameRu, } try { if (id && id !== 'new') { await mockDb.updateNeighborhood(id, data) } else { await mockDb.createNeighborhood(data) } revalidatePath('/admin/neighborhoods') return { success: true, error: undefined } } catch (err: any) { return { success: false, error: err.message || 'Mahalle kaydedilemedi.' } } } // AI Itinerary Planner Actions (Phase 3) import crypto from 'crypto' import { generateItineraryContent, type ItineraryCandidate } from '@/lib/deepseek' const MAX_ITINERARY_CANDIDATES = 30 export async function generateItineraryAction(days: number, style: string, neighborhoodSlugs: string[], locale: string) { const sorted = [...neighborhoodSlugs].sort() const paramsHash = crypto.createHash('md5').update(JSON.stringify({ days, style, neighborhoodSlugs: sorted, locale })).digest('hex') const existing = await mockDb.getItineraryByHash(paramsHash) if (existing) { return { success: true, id: existing.id } } // Cache hit above is free; only a genuinely new combination reaches the // paid DeepSeek call, so rate-limit per IP to bound abuse cost. const ip = await getClientIp() if (!checkRateLimit(`itinerary:${ip}`, 10, 60 * 60_000)) { return { success: false, error: 'Çok fazla plan oluşturuldu. Lütfen bir süre sonra tekrar deneyin.' } } const allListings = await mockDb.getListings() const matchingListings = allListings.filter(l => !l.deletedAt && l.isLocalApproved && (sorted.length === 0 || (l.neighborhood && sorted.includes(l.neighborhood.slug))) ) const pool = (matchingListings.length > 0 ? matchingListings : allListings.filter(l => !l.deletedAt && l.isLocalApproved)) .sort((a, b) => Number(b.isFeatured) - Number(a.isFeatured)) .slice(0, MAX_ITINERARY_CANDIDATES) if (pool.length === 0) { return { success: false, error: 'Seçtiğiniz bölgelerde yerel onaylı mekan bulunamadı.' } } const nameKey = locale === 'en' ? 'nameEn' : locale === 'ru' ? 'nameRu' : 'nameTr' const descKey = locale === 'en' ? 'descriptionEn' : locale === 'ru' ? 'descriptionRu' : 'descriptionTr' const candidates: ItineraryCandidate[] = pool.map(l => ({ id: l.id, name: (l as any)[nameKey] || l.nameTr, description: (l as any)[descKey] || l.descriptionTr, category: l.category?.nameTr || '', categorySlug: l.category?.slug || 'isletme', neighborhood: l.neighborhood?.nameTr || '', address: l.address, priceRange: l.priceRange, rating: l.rating ?? null, slug: l.slug, isFeatured: l.isFeatured, })) let content: string try { content = await generateItineraryContent({ days, style, locale, candidates }) } catch (e: any) { console.error('DeepSeek itinerary generation error:', e) return { success: false, error: 'Planınız oluşturulamadı, lütfen birazdan tekrar deneyin.' } } const newItin = await mockDb.createItinerary({ paramsHash, params: { days, style, neighborhoodSlugs: sorted, locale }, content, listingIds: pool.map(l => l.id) }) return { success: true, id: newItin.id } } export async function createOrUpdateCategoryAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string const slug = formData.get('slug') as string const nameTr = formData.get('nameTr') as string const nameEn = formData.get('nameEn') as string const nameRu = formData.get('nameRu') as string if (!slug || !nameTr || !nameEn || !nameRu) { throw new Error('Eksik alanlar var') } if (id && id !== 'new') { await mockDb.updateCategory(id, { slug, nameTr, nameEn, nameRu }) } else { await mockDb.createCategory({ slug, nameTr, nameEn, nameRu }) } revalidatePath('/admin/categories') redirect('/tr/admin/categories') } export async function deleteCategoryAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string if (!id) return await mockDb.deleteCategory(id) revalidatePath('/admin/categories') } export async function deleteSubmissionAction(id: string) { await requireAdmin() await mockDb.deleteSubmission(id) revalidatePath('/admin/submissions') return { success: true, error: undefined } } export async function deleteMessageAction(id: string) { await requireAdmin() await mockDb.deleteMessage(id) revalidatePath('/admin/messages') return { success: true, error: undefined } } export async function searchListingsAction(query: string) { if (!query || query.length < 2) return [] const allListings = await mockDb.getListings() const q = query.toLowerCase() const results = allListings.filter(l => !l.deletedAt && ( l.nameTr.toLowerCase().includes(q) || l.nameEn?.toLowerCase().includes(q) || l.nameRu?.toLowerCase().includes(q) ) ).slice(0, 5) // Return max 5 suggestions return results.map(l => ({ id: l.id, nameTr: l.nameTr, nameEn: l.nameEn, nameRu: l.nameRu, slug: l.slug, categorySlug: l.category?.slug || 'restoran' })) } export async function saveWidgetPartnerAction(formData: FormData) { await requireAdmin() const id = formData.get('id') as string | null const name = formData.get('name') as string const url = formData.get('url') as string const neighborhoodSlug = formData.get('neighborhoodSlug') as string | null const isActive = formData.get('isActive') === 'true' const isHidden = formData.get('isHidden') === 'true' if (!name || !url) { return { error: 'Lütfen zorunlu alanları doldurun.' } } const data = { name, url, neighborhoodSlug: neighborhoodSlug || null, isActive, isHidden } try { if (id && id !== 'new') { await mockDb.updateWidgetPartner(id, data) } else { await mockDb.createWidgetPartner(data) } revalidatePath('/admin/widget-partners') return { success: true, error: undefined } } catch (error: any) { return { error: error.message || 'Bir hata oluştu.' } } } export async function deleteWidgetPartnerAction(id: string) { await requireAdmin() await mockDb.deleteWidgetPartner(id) revalidatePath('/admin/widget-partners') return { success: true, error: undefined } }