sms entegrasyonu ve ana sayfa işlemleri
This commit is contained in:
@@ -21,6 +21,8 @@ const SECTIONS = [
|
||||
{ id: 'general', label: 'Genel Ayarlar' },
|
||||
{ id: 'home', label: 'Anasayfa' },
|
||||
{ id: 'contact', label: 'İletişim' },
|
||||
{ id: 'seo', label: 'SEO Ayarları' },
|
||||
{ id: 'scripts', label: 'Script & Analitik' },
|
||||
]
|
||||
|
||||
export function ContentForm({ initialContent }: ContentFormProps) {
|
||||
|
||||
187
components/dashboard/customer-form.tsx
Normal file
187
components/dashboard/customer-form.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
import { Customer, CustomerInsert } from "@/types/customer"
|
||||
import { addCustomer, updateCustomer } from "@/lib/customers/actions"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Plus, Loader2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const formSchema = z.object({
|
||||
full_name: z.string().min(2, "Ad soyad en az 2 karakter olmalıdır"),
|
||||
email: z.string().email("Geçersiz e-posta adresi").or(z.literal("")).optional(),
|
||||
phone: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
interface CustomerFormProps {
|
||||
customer?: Customer
|
||||
trigger?: React.ReactNode
|
||||
}
|
||||
|
||||
export function CustomerForm({ customer, trigger }: CustomerFormProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
full_name: customer?.full_name || "",
|
||||
email: customer?.email || "",
|
||||
phone: customer?.phone || "",
|
||||
address: customer?.address || "",
|
||||
notes: customer?.notes || "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const customerData = {
|
||||
...values,
|
||||
email: values.email || null,
|
||||
phone: values.phone || null,
|
||||
address: values.address || null,
|
||||
notes: values.notes || null,
|
||||
} as CustomerInsert
|
||||
|
||||
let result
|
||||
if (customer) {
|
||||
result = await updateCustomer(customer.id, customerData)
|
||||
} else {
|
||||
result = await addCustomer(customerData)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(customer ? "Müşteri güncellendi" : "Müşteri eklendi")
|
||||
setOpen(false)
|
||||
form.reset()
|
||||
} else {
|
||||
toast.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
toast.error("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger || (
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Müşteri
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{customer ? "Müşteriyi Düzenle" : "Yeni Müşteri Ekle"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Müşteri bilgilerini aşağıdan yönetebilirsiniz.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="full_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Ad Soyad</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Ad Soyad" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Posta</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="ornek@site.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="0555..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Adres</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Adres..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Özel Notlar</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Notlar..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Kaydet
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LayoutDashboard, Package, ShoppingCart, Settings, Globe, Tags } from "lucide-react"
|
||||
import { LayoutDashboard, Package, ShoppingCart, Settings, Globe, Tags, Users, MessageSquare, History } from "lucide-react"
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
@@ -16,15 +16,25 @@ const sidebarItems = [
|
||||
href: "/dashboard/products",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: "Kategoriler",
|
||||
href: "/dashboard/categories",
|
||||
icon: Tags,
|
||||
},
|
||||
{
|
||||
title: "Siparişler",
|
||||
href: "/dashboard/orders",
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
title: "Kategoriler",
|
||||
href: "/dashboard/categories",
|
||||
icon: Tags,
|
||||
title: "Müşteriler",
|
||||
href: "/dashboard/customers",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "SMS Gönder",
|
||||
href: "/dashboard/sms",
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
title: "Ayarlar",
|
||||
@@ -51,7 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
Yönetim
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
{sidebarItems.map((item) => (
|
||||
{sidebarItems.filter(i => !i.href.includes('/sms')).map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
@@ -66,6 +76,34 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2">
|
||||
<h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">
|
||||
SMS
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/dashboard/sms"
|
||||
className={cn(
|
||||
"flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors",
|
||||
pathname === "/dashboard/sms" ? "bg-slate-100 dark:bg-slate-800 text-primary" : "text-slate-500 dark:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Yeni SMS
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard/sms/logs"
|
||||
className={cn(
|
||||
"flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors",
|
||||
pathname === "/dashboard/sms/logs" ? "bg-slate-100 dark:bg-slate-800 text-primary" : "text-slate-500 dark:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Geçmiş
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
198
components/dashboard/sms-page-client.tsx
Normal file
198
components/dashboard/sms-page-client.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
import { Customer } from "@/types/customer"
|
||||
import { sendBulkSms } from "@/lib/sms/actions"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { Loader2, Send } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
|
||||
const formSchema = z.object({
|
||||
manualNumbers: z.string().optional(),
|
||||
message: z.string().min(1, "Mesaj içeriği boş olamaz").max(900, "Mesaj çok uzun (max 900 karakter)"),
|
||||
selectedCustomers: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
interface SmsPageProps {
|
||||
customers: Customer[]
|
||||
}
|
||||
|
||||
export default function SmsPageClient({ customers }: SmsPageProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectAll, setSelectAll] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
manualNumbers: "",
|
||||
message: "",
|
||||
selectedCustomers: []
|
||||
},
|
||||
})
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
setSelectAll(checked)
|
||||
if (checked) {
|
||||
const allPhones = customers.map(c => c.phone).filter(Boolean) as string[]
|
||||
form.setValue('selectedCustomers', allPhones)
|
||||
} else {
|
||||
form.setValue('selectedCustomers', [])
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const manualPhones = values.manualNumbers
|
||||
?.split(/[,\n]/) // Split by comma or newline
|
||||
.map(p => p.trim())
|
||||
.filter(p => p !== "") || []
|
||||
|
||||
const customerPhones = values.selectedCustomers || []
|
||||
const allPhones = [...manualPhones, ...customerPhones]
|
||||
|
||||
if (allPhones.length === 0) {
|
||||
toast.error("Lütfen en az bir alıcı seçin veya numara girin.")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await sendBulkSms(allPhones, values.message)
|
||||
if (result.success) {
|
||||
toast.success(result.message)
|
||||
form.reset()
|
||||
setSelectAll(false)
|
||||
} else {
|
||||
toast.error(result.error || "SMS gönderilirken hata oluştu")
|
||||
}
|
||||
} catch {
|
||||
toast.error("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const watchedSelected = form.watch("selectedCustomers") || []
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<h2 className="text-3xl font-bold tracking-tight">SMS Gönderimi</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Mesaj Bilgileri</CardTitle>
|
||||
<CardDescription>
|
||||
Toplu veya tekil SMS gönderin. (Türkçe karakter desteklenir)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="sms-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Manuel Numaralar</Label>
|
||||
<Textarea
|
||||
placeholder="5551234567, 5329876543 (Virgül veya alt satır ile ayırın)"
|
||||
{...form.register("manualNumbers")}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Veritabanında olmayan numaraları buraya girebilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Gönderilecek Mesaj</Label>
|
||||
<Textarea
|
||||
className="min-h-[120px]"
|
||||
placeholder="Mesajınızı buraya yazın..."
|
||||
{...form.register("message")}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Türkçe karakterler otomatik desteklenir.</span>
|
||||
<span>{form.watch("message")?.length || 0} / 900</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<div className="p-4 bg-slate-50 dark:bg-slate-900 rounded-md">
|
||||
<h4 className="font-semibold mb-2">Özet</h4>
|
||||
<ul className="list-disc list-inside text-sm">
|
||||
<li>Manuel: {(form.watch("manualNumbers")?.split(/[,\n]/).filter(x => x.trim()).length || 0)} Kişi</li>
|
||||
<li>Seçili Müşteri: {watchedSelected.length} Kişi</li>
|
||||
<li className="font-bold mt-1">Toplam: {(form.watch("manualNumbers")?.split(/[,\n]/).filter(x => x.trim()).length || 0) + watchedSelected.length} Kişi</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="sms-form" type="submit" disabled={loading} className="w-full">
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Send className="mr-2 h-4 w-4" />}
|
||||
Gönderimi Başlat
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>Müşteri Listesi</CardTitle>
|
||||
<CardDescription>
|
||||
Listeden toplu seçim yapabilirsiniz.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-[400px]">
|
||||
<div className="flex items-center space-x-2 mb-4 pb-4 border-b">
|
||||
<Checkbox
|
||||
id="select-all"
|
||||
checked={selectAll}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
<Label htmlFor="select-all" className="font-bold">Tümünü Seç ({customers.length})</Label>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[400px] w-full pr-4">
|
||||
<div className="space-y-2">
|
||||
{customers.length === 0 && <p className="text-muted-foreground">Kayıtlı müşteri bulunamadı.</p>}
|
||||
{customers.map((customer) => (
|
||||
<div key={customer.id} className="flex items-start space-x-2 py-2 hover:bg-slate-50 dark:hover:bg-slate-900 rounded px-2">
|
||||
<Checkbox
|
||||
id={`customer-${customer.id}`}
|
||||
checked={watchedSelected.includes(customer.phone || "")}
|
||||
disabled={!customer.phone}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = form.getValues("selectedCustomers") || []
|
||||
if (checked) {
|
||||
form.setValue("selectedCustomers", [...current, customer.phone || ""])
|
||||
} else {
|
||||
form.setValue("selectedCustomers", current.filter(p => p !== customer.phone))
|
||||
setSelectAll(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="grid gap-1.5 leading-none">
|
||||
<Label
|
||||
htmlFor={`customer-${customer.id}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{customer.full_name}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{customer.phone || "Telefon Yok"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user