|
|
@@ -2,67 +2,388 @@
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
-import { Textarea } from "@/components/ui/textarea";
|
|
|
+import { Input } from "@/components/ui/input";
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
-import { Mic, MessageSquare, Store, Loader2, Bot } from "lucide-react";
|
|
|
-import { useState, useEffect } from "react";
|
|
|
+import { Mic, MessageSquare, Store, Loader2, Bot, Play, Pause, User, Users, Clock, Calendar, Trash2, Plus, X } from "lucide-react";
|
|
|
+import { useState, useEffect, useRef } from "react";
|
|
|
import { API_URL } from "@/lib/config";
|
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
import { useShop } from "@/components/context/ShopContext";
|
|
|
import { LoadingScreen } from "@/components/ui/loading-screen";
|
|
|
+import { supabase } from "@/lib/supabase";
|
|
|
+import { cn } from "@/lib/utils";
|
|
|
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
|
+import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
|
|
+import { format } from "date-fns";
|
|
|
+import { hu, de, enUS } from "date-fns/locale";
|
|
|
+
|
|
|
+interface AIVoice {
|
|
|
+ id: string;
|
|
|
+ provider_voice_id: string;
|
|
|
+ sample_url: string;
|
|
|
+ gender: 'male' | 'female' | 'neutral';
|
|
|
+ name: string;
|
|
|
+}
|
|
|
|
|
|
interface AIConfig {
|
|
|
voice_type: string;
|
|
|
- speaking_speed: string;
|
|
|
- accent_language: string;
|
|
|
greeting_message: string;
|
|
|
- business_hours_mode: boolean;
|
|
|
- local_currency_support: boolean;
|
|
|
- escalation_policy: string;
|
|
|
}
|
|
|
|
|
|
+interface DayHours {
|
|
|
+ is_open: boolean;
|
|
|
+ open: string;
|
|
|
+ close: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface DailyHours {
|
|
|
+ monday: DayHours;
|
|
|
+ tuesday: DayHours;
|
|
|
+ wednesday: DayHours;
|
|
|
+ thursday: DayHours;
|
|
|
+ friday: DayHours;
|
|
|
+ saturday: DayHours;
|
|
|
+ sunday: DayHours;
|
|
|
+}
|
|
|
+
|
|
|
+interface BusinessHours {
|
|
|
+ id?: string;
|
|
|
+ store_id: string;
|
|
|
+ is_enabled: boolean;
|
|
|
+ daily_hours: DailyHours;
|
|
|
+}
|
|
|
+
|
|
|
+interface SpecialHours {
|
|
|
+ id?: string;
|
|
|
+ store_id: string;
|
|
|
+ date: string;
|
|
|
+ opening_time: string | null;
|
|
|
+ closing_time: string | null;
|
|
|
+ is_closed: boolean;
|
|
|
+ note: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+const WEEKDAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
|
|
|
+type WeekDay = typeof WEEKDAYS[number];
|
|
|
+
|
|
|
+const TIME_OPTIONS = [
|
|
|
+ '00:00', '00:30', '01:00', '01:30', '02:00', '02:30', '03:00', '03:30',
|
|
|
+ '04:00', '04:30', '05:00', '05:30', '06:00', '06:30', '07:00', '07:30',
|
|
|
+ '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
|
|
|
+ '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30',
|
|
|
+ '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30',
|
|
|
+ '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30'
|
|
|
+];
|
|
|
+
|
|
|
+const DEFAULT_DAILY_HOURS: DailyHours = {
|
|
|
+ monday: { is_open: true, open: '09:00', close: '18:00' },
|
|
|
+ tuesday: { is_open: true, open: '09:00', close: '18:00' },
|
|
|
+ wednesday: { is_open: true, open: '09:00', close: '18:00' },
|
|
|
+ thursday: { is_open: true, open: '09:00', close: '18:00' },
|
|
|
+ friday: { is_open: true, open: '09:00', close: '18:00' },
|
|
|
+ saturday: { is_open: false, open: '10:00', close: '16:00' },
|
|
|
+ sunday: { is_open: false, open: '10:00', close: '14:00' }
|
|
|
+};
|
|
|
+
|
|
|
+// Validation regex to block emails and URLs
|
|
|
+const INVALID_INPUT_PATTERN = /[@]|https?:\/\/|www\.|\.com|\.hu|\.net|\.org|\.eu|\.io|\.co/i;
|
|
|
+
|
|
|
+// Helper to get date-fns locale based on i18n language
|
|
|
+const getDateLocale = (lang: string) => {
|
|
|
+ switch (lang) {
|
|
|
+ case 'hu': return hu;
|
|
|
+ case 'de': return de;
|
|
|
+ default: return enUS;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
export function AIConfigContent() {
|
|
|
- const { t } = useTranslation();
|
|
|
+ const { t, i18n } = useTranslation();
|
|
|
const { toast } = useToast();
|
|
|
const { selectedShop, stores, isLoading } = useShop();
|
|
|
+ const dateLocale = getDateLocale(i18n.language);
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
+ const [voices, setVoices] = useState<AIVoice[]>([]);
|
|
|
+ const [voicesLoading, setVoicesLoading] = useState(true);
|
|
|
+ const [genderFilter, setGenderFilter] = useState<'all' | 'male' | 'female' | 'neutral'>('all');
|
|
|
+ const [playingVoiceId, setPlayingVoiceId] = useState<string | null>(null);
|
|
|
+ const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
|
+ const [greetingError, setGreetingError] = useState<string | null>(null);
|
|
|
+
|
|
|
const [aiConfig, setAiConfig] = useState<AIConfig>({
|
|
|
- voice_type: "sarah",
|
|
|
- speaking_speed: "normal",
|
|
|
- accent_language: "us-english",
|
|
|
- greeting_message: "",
|
|
|
- business_hours_mode: true,
|
|
|
- local_currency_support: true,
|
|
|
- escalation_policy: "medium"
|
|
|
+ voice_type: "",
|
|
|
+ greeting_message: ""
|
|
|
+ });
|
|
|
+
|
|
|
+ // Business hours state
|
|
|
+ const [businessHours, setBusinessHours] = useState<BusinessHours>({
|
|
|
+ store_id: "",
|
|
|
+ is_enabled: false,
|
|
|
+ daily_hours: DEFAULT_DAILY_HOURS
|
|
|
});
|
|
|
+ const [specialHours, setSpecialHours] = useState<SpecialHours[]>([]);
|
|
|
+ const [businessHoursLoading, setBusinessHoursLoading] = useState(false);
|
|
|
+
|
|
|
+ // New special hours form state
|
|
|
+ const [newSpecialDate, setNewSpecialDate] = useState<Date | undefined>(undefined);
|
|
|
+ const [newSpecialNote, setNewSpecialNote] = useState("");
|
|
|
+ const [newSpecialIsClosed, setNewSpecialIsClosed] = useState(false);
|
|
|
+ const [newSpecialOpeningTime, setNewSpecialOpeningTime] = useState("09:00");
|
|
|
+ const [newSpecialClosingTime, setNewSpecialClosingTime] = useState("18:00");
|
|
|
+ const [calendarOpen, setCalendarOpen] = useState(false);
|
|
|
+
|
|
|
+ // Load voices from database
|
|
|
+ useEffect(() => {
|
|
|
+ loadVoices();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const loadVoices = async () => {
|
|
|
+ setVoicesLoading(true);
|
|
|
+ try {
|
|
|
+ const { data, error } = await supabase
|
|
|
+ .from('ai_voices')
|
|
|
+ .select('id, provider_voice_id, sample_url, gender, name')
|
|
|
+ .eq('is_enabled', true)
|
|
|
+ .order('name', { ascending: true });
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ console.error('Error loading voices:', error);
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('aiConfig.voiceSettings.loadError', 'Failed to load voices'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setVoices(data || []);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading voices:', error);
|
|
|
+ } finally {
|
|
|
+ setVoicesLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle voice sample playback
|
|
|
+ const handlePlayVoice = (voice: AIVoice) => {
|
|
|
+ if (playingVoiceId === voice.id) {
|
|
|
+ if (audioRef.current) {
|
|
|
+ audioRef.current.pause();
|
|
|
+ audioRef.current.currentTime = 0;
|
|
|
+ }
|
|
|
+ setPlayingVoiceId(null);
|
|
|
+ } else {
|
|
|
+ if (audioRef.current) {
|
|
|
+ audioRef.current.pause();
|
|
|
+ }
|
|
|
+ audioRef.current = new Audio(voice.sample_url);
|
|
|
+ audioRef.current.onended = () => setPlayingVoiceId(null);
|
|
|
+ audioRef.current.onerror = () => {
|
|
|
+ setPlayingVoiceId(null);
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('aiConfig.voiceSettings.playError', 'Failed to play voice sample'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ };
|
|
|
+ audioRef.current.play();
|
|
|
+ setPlayingVoiceId(voice.id);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Stop audio on unmount
|
|
|
+ useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ if (audioRef.current) {
|
|
|
+ audioRef.current.pause();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // Filter voices by gender
|
|
|
+ const filteredVoices = voices.filter(voice =>
|
|
|
+ genderFilter === 'all' || voice.gender === genderFilter
|
|
|
+ );
|
|
|
|
|
|
- // Load AI config when selected shop changes
|
|
|
+ // Load AI config and business hours when selected shop changes
|
|
|
useEffect(() => {
|
|
|
if (selectedShop) {
|
|
|
loadAIConfig(selectedShop);
|
|
|
+ loadBusinessHours(selectedShop.id);
|
|
|
}
|
|
|
}, [selectedShop]);
|
|
|
|
|
|
const loadAIConfig = (store: any) => {
|
|
|
- // Load AI config from store's alt_data or use defaults
|
|
|
const config = store.alt_data?.ai_config || {};
|
|
|
-
|
|
|
setAiConfig({
|
|
|
- voice_type: config.voice_type || "sarah",
|
|
|
- speaking_speed: config.speaking_speed || "normal",
|
|
|
- accent_language: config.accent_language || "us-english",
|
|
|
- greeting_message: config.greeting_message || `Hello! Thank you for calling ${store.store_name || 'our store'}. I'm your AI assistant, and I'm here to help you with any questions about your order or our products. How can I assist you today?`,
|
|
|
- business_hours_mode: config.business_hours_mode !== undefined ? config.business_hours_mode : true,
|
|
|
- local_currency_support: config.local_currency_support !== undefined ? config.local_currency_support : true,
|
|
|
- escalation_policy: config.escalation_policy || "medium"
|
|
|
+ voice_type: config.voice_type || "",
|
|
|
+ greeting_message: config.greeting_message || `Hello! Thank you for calling ${store.store_name || 'our store'}. How can I help you?`
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ const loadBusinessHours = async (storeId: string) => {
|
|
|
+ setBusinessHoursLoading(true);
|
|
|
+ try {
|
|
|
+ // Load business hours
|
|
|
+ const { data: hoursData, error: hoursError } = await supabase
|
|
|
+ .from('store_business_hours')
|
|
|
+ .select('*')
|
|
|
+ .eq('store_id', storeId)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (hoursError && hoursError.code !== 'PGRST116') {
|
|
|
+ console.error('Error loading business hours:', hoursError);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hoursData) {
|
|
|
+ setBusinessHours({
|
|
|
+ ...hoursData,
|
|
|
+ daily_hours: hoursData.daily_hours || DEFAULT_DAILY_HOURS
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ setBusinessHours({
|
|
|
+ store_id: storeId,
|
|
|
+ is_enabled: false,
|
|
|
+ daily_hours: DEFAULT_DAILY_HOURS
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load special hours (only future dates)
|
|
|
+ const { data: specialData, error: specialError } = await supabase
|
|
|
+ .from('store_special_hours')
|
|
|
+ .select('*')
|
|
|
+ .eq('store_id', storeId)
|
|
|
+ .gte('date', new Date().toISOString().split('T')[0])
|
|
|
+ .order('date', { ascending: true });
|
|
|
+
|
|
|
+ if (specialError) {
|
|
|
+ console.error('Error loading special hours:', specialError);
|
|
|
+ }
|
|
|
+
|
|
|
+ setSpecialHours(specialData || []);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading business hours:', error);
|
|
|
+ } finally {
|
|
|
+ setBusinessHoursLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Validate greeting message
|
|
|
+ const validateGreetingMessage = (value: string) => {
|
|
|
+ if (INVALID_INPUT_PATTERN.test(value)) {
|
|
|
+ setGreetingError(t('aiConfig.conversationBehavior.greetingError', 'No emails, URLs, or website addresses allowed'));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ setGreetingError(null);
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleGreetingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
+ const value = e.target.value;
|
|
|
+ if (value.length <= 255) {
|
|
|
+ validateGreetingMessage(value);
|
|
|
+ setAiConfig({ ...aiConfig, greeting_message: value });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Add special hours
|
|
|
+ const handleAddSpecialHours = async () => {
|
|
|
+ if (!newSpecialDate || !selectedShop) return;
|
|
|
+
|
|
|
+ const dateStr = format(newSpecialDate, 'yyyy-MM-dd');
|
|
|
+
|
|
|
+ // Check if date already exists
|
|
|
+ if (specialHours.some(sh => sh.date === dateStr)) {
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('aiConfig.businessHours.dateExists', 'This date already has special hours'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const newEntry: Omit<SpecialHours, 'id'> = {
|
|
|
+ store_id: selectedShop.id,
|
|
|
+ date: dateStr,
|
|
|
+ opening_time: newSpecialIsClosed ? null : newSpecialOpeningTime,
|
|
|
+ closing_time: newSpecialIsClosed ? null : newSpecialClosingTime,
|
|
|
+ is_closed: newSpecialIsClosed,
|
|
|
+ note: newSpecialNote || null
|
|
|
+ };
|
|
|
+
|
|
|
+ const { data, error } = await supabase
|
|
|
+ .from('store_special_hours')
|
|
|
+ .insert(newEntry)
|
|
|
+ .select()
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (error) throw error;
|
|
|
+
|
|
|
+ setSpecialHours([...specialHours, data].sort((a, b) => a.date.localeCompare(b.date)));
|
|
|
+
|
|
|
+ // Reset form
|
|
|
+ setNewSpecialDate(undefined);
|
|
|
+ setNewSpecialNote("");
|
|
|
+ setNewSpecialIsClosed(false);
|
|
|
+ setNewSpecialOpeningTime("09:00");
|
|
|
+ setNewSpecialClosingTime("18:00");
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: t('common.success'),
|
|
|
+ description: t('aiConfig.businessHours.specialAdded', 'Special hours added'),
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error adding special hours:', error);
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('common.tryAgain'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Delete special hours
|
|
|
+ const handleDeleteSpecialHours = async (id: string) => {
|
|
|
+ try {
|
|
|
+ const { error } = await supabase
|
|
|
+ .from('store_special_hours')
|
|
|
+ .delete()
|
|
|
+ .eq('id', id);
|
|
|
+
|
|
|
+ if (error) throw error;
|
|
|
+
|
|
|
+ setSpecialHours(specialHours.filter(sh => sh.id !== id));
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: t('common.success'),
|
|
|
+ description: t('aiConfig.businessHours.specialDeleted', 'Special hours deleted'),
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error deleting special hours:', error);
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('common.tryAgain'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const handleSaveConfig = async () => {
|
|
|
if (!selectedShop) return;
|
|
|
|
|
|
+ // Validate greeting message before saving
|
|
|
+ if (!validateGreetingMessage(aiConfig.greeting_message)) {
|
|
|
+ toast({
|
|
|
+ title: t('common.error'),
|
|
|
+ description: t('aiConfig.conversationBehavior.greetingError', 'No emails, URLs, or website addresses allowed'),
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
setSaving(true);
|
|
|
try {
|
|
|
const sessionData = localStorage.getItem('session_data');
|
|
|
@@ -72,7 +393,7 @@ export function AIConfigContent() {
|
|
|
|
|
|
const session = JSON.parse(sessionData);
|
|
|
|
|
|
- // Update store's alt_data with AI config
|
|
|
+ // Save AI config
|
|
|
const response = await fetch(`${API_URL}/api/stores/${selectedShop.id}/ai-config`, {
|
|
|
method: 'PUT',
|
|
|
headers: {
|
|
|
@@ -86,6 +407,22 @@ export function AIConfigContent() {
|
|
|
throw new Error('Failed to save configuration');
|
|
|
}
|
|
|
|
|
|
+ // Save business hours (upsert)
|
|
|
+ const { error: hoursError } = await supabase
|
|
|
+ .from('store_business_hours')
|
|
|
+ .upsert({
|
|
|
+ ...businessHours,
|
|
|
+ store_id: selectedShop.id,
|
|
|
+ updated_at: new Date().toISOString()
|
|
|
+ }, {
|
|
|
+ onConflict: 'store_id'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (hoursError) {
|
|
|
+ console.error('Error saving business hours:', hoursError);
|
|
|
+ throw hoursError;
|
|
|
+ }
|
|
|
+
|
|
|
toast({
|
|
|
title: t('common.success'),
|
|
|
description: t('aiConfig.saveConfiguration'),
|
|
|
@@ -137,84 +474,172 @@ export function AIConfigContent() {
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <div className="flex-1 space-y-6 p-8 bg-slate-900">
|
|
|
+ <div className="flex-1 space-y-6 p-8 bg-slate-900 relative">
|
|
|
+ {/* Floating Save Button */}
|
|
|
+ <div className="fixed top-20 right-8 z-50">
|
|
|
+ <Button
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white shadow-lg"
|
|
|
+ onClick={handleSaveConfig}
|
|
|
+ disabled={saving}
|
|
|
+ >
|
|
|
+ {saving ? (
|
|
|
+ <>
|
|
|
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
+ {t('aiConfig.saving')}
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ t('aiConfig.saveConfiguration')
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div className="flex items-center justify-between">
|
|
|
<div>
|
|
|
<h2 className="text-3xl font-bold tracking-tight text-white">{t('aiConfig.title')}</h2>
|
|
|
<p className="text-slate-400">{t('aiConfig.subtitle')}</p>
|
|
|
</div>
|
|
|
- <div className="flex gap-3">
|
|
|
- <Button
|
|
|
- className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
- onClick={handleSaveConfig}
|
|
|
- disabled={saving}
|
|
|
- >
|
|
|
- {saving ? (
|
|
|
- <>
|
|
|
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
- {t('aiConfig.saving')}
|
|
|
- </>
|
|
|
- ) : (
|
|
|
- t('aiConfig.saveConfiguration')
|
|
|
- )}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
<div className="grid gap-6">
|
|
|
+ {/* Voice Settings Card */}
|
|
|
<Card className="bg-slate-800 border-slate-700">
|
|
|
<CardHeader>
|
|
|
- <div className="flex items-center gap-3">
|
|
|
- <Mic className="w-6 h-6 text-cyan-500" />
|
|
|
- <CardTitle className="text-white">{t('aiConfig.voiceSettings.title')}</CardTitle>
|
|
|
- </div>
|
|
|
- <p className="text-slate-400">{t('aiConfig.voiceSettings.subtitle')} {selectedShop.store_name || 'your store'}</p>
|
|
|
- </CardHeader>
|
|
|
- <CardContent className="space-y-6">
|
|
|
- <div className="grid gap-6 md:grid-cols-2">
|
|
|
- <div className="space-y-2">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.voiceSettings.voiceType')}</Label>
|
|
|
- <Select value={aiConfig.voice_type} onValueChange={(value) => setAiConfig({ ...aiConfig, voice_type: value })}>
|
|
|
- <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
|
|
- <SelectValue />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
- <SelectItem value="sarah">{t('aiConfig.voiceSettings.voices.sarah')}</SelectItem>
|
|
|
- <SelectItem value="james">{t('aiConfig.voiceSettings.voices.james')}</SelectItem>
|
|
|
- <SelectItem value="emma">{t('aiConfig.voiceSettings.voices.emma')}</SelectItem>
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <Mic className="w-6 h-6 text-cyan-500" />
|
|
|
+ <div>
|
|
|
+ <CardTitle className="text-white">{t('aiConfig.voiceSettings.title')}</CardTitle>
|
|
|
+ <p className="text-slate-400 text-sm mt-1">{t('aiConfig.voiceSettings.subtitle')} {selectedShop.store_name || 'your store'}</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div className="space-y-2">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.voiceSettings.speakingSpeed')}</Label>
|
|
|
- <Select value={aiConfig.speaking_speed} onValueChange={(value) => setAiConfig({ ...aiConfig, speaking_speed: value })}>
|
|
|
- <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
|
|
+ {/* Gender Filter */}
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Label className="text-slate-400 text-sm">{t('aiConfig.voiceSettings.filterByGender', 'Filter:')}</Label>
|
|
|
+ <Select value={genderFilter} onValueChange={(value: 'all' | 'male' | 'female' | 'neutral') => setGenderFilter(value)}>
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[140px]">
|
|
|
<SelectValue />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent className="bg-slate-700 border-slate-600">
|
|
|
- <SelectItem value="slow">{t('aiConfig.voiceSettings.speeds.slow')}</SelectItem>
|
|
|
- <SelectItem value="normal">{t('aiConfig.voiceSettings.speeds.normal')}</SelectItem>
|
|
|
- <SelectItem value="fast">{t('aiConfig.voiceSettings.speeds.fast')}</SelectItem>
|
|
|
+ <SelectItem value="all">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Users className="w-4 h-4" />
|
|
|
+ {t('aiConfig.voiceSettings.genders.all', 'All')}
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ <SelectItem value="female">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <User className="w-4 h-4 text-pink-400" />
|
|
|
+ {t('aiConfig.voiceSettings.genders.female', 'Female')}
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ <SelectItem value="male">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <User className="w-4 h-4 text-blue-400" />
|
|
|
+ {t('aiConfig.voiceSettings.genders.male', 'Male')}
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ <SelectItem value="neutral">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <User className="w-4 h-4 text-purple-400" />
|
|
|
+ {t('aiConfig.voiceSettings.genders.neutral', 'Neutral')}
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
</SelectContent>
|
|
|
</Select>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="space-y-2">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.voiceSettings.accentLanguage')}</Label>
|
|
|
- <Select value={aiConfig.accent_language} onValueChange={(value) => setAiConfig({ ...aiConfig, accent_language: value })}>
|
|
|
- <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
|
|
- <SelectValue />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
- <SelectItem value="us-english">{t('aiConfig.voiceSettings.accents.usEnglish')}</SelectItem>
|
|
|
- <SelectItem value="uk-english">{t('aiConfig.voiceSettings.accents.ukEnglish')}</SelectItem>
|
|
|
- <SelectItem value="australian">{t('aiConfig.voiceSettings.accents.australian')}</SelectItem>
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ {/* Voice Selection Cards - Horizontal Single Line Layout */}
|
|
|
+ <div className="space-y-3">
|
|
|
+ <Label className="text-slate-300">{t('aiConfig.voiceSettings.selectVoice', 'Select Voice')}</Label>
|
|
|
+ {voicesLoading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <Loader2 className="w-6 h-6 animate-spin text-cyan-500" />
|
|
|
+ <span className="ml-2 text-slate-400">{t('aiConfig.voiceSettings.loadingVoices', 'Loading voices...')}</span>
|
|
|
+ </div>
|
|
|
+ ) : filteredVoices.length === 0 ? (
|
|
|
+ <div className="text-center py-8 text-slate-400">
|
|
|
+ {t('aiConfig.voiceSettings.noVoicesFound', 'No voices found for the selected filter.')}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 max-h-[300px] overflow-y-auto pr-2">
|
|
|
+ {filteredVoices.map((voice) => (
|
|
|
+ <div
|
|
|
+ key={voice.id}
|
|
|
+ className={cn(
|
|
|
+ "flex items-center gap-2 rounded-lg border-2 p-2 cursor-pointer transition-all",
|
|
|
+ aiConfig.voice_type === voice.provider_voice_id
|
|
|
+ ? "border-cyan-500 bg-slate-700/80"
|
|
|
+ : "border-slate-600 bg-slate-700/40 hover:border-slate-500 hover:bg-slate-700/60"
|
|
|
+ )}
|
|
|
+ onClick={() => setAiConfig({ ...aiConfig, voice_type: voice.provider_voice_id })}
|
|
|
+ >
|
|
|
+ {/* Avatar with gender color */}
|
|
|
+ <div className={cn(
|
|
|
+ "w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
|
|
|
+ voice.gender === 'female' ? "bg-pink-500/20" :
|
|
|
+ voice.gender === 'male' ? "bg-blue-500/20" :
|
|
|
+ "bg-purple-500/20"
|
|
|
+ )}>
|
|
|
+ <User className={cn(
|
|
|
+ "w-4 h-4",
|
|
|
+ voice.gender === 'female' ? "text-pink-400" :
|
|
|
+ voice.gender === 'male' ? "text-blue-400" :
|
|
|
+ "text-purple-400"
|
|
|
+ )} />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Name */}
|
|
|
+ <span className="text-white font-medium text-sm truncate flex-1">
|
|
|
+ {voice.name}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ {/* Gender badge */}
|
|
|
+ <span className={cn(
|
|
|
+ "text-xs px-1.5 py-0.5 rounded flex-shrink-0",
|
|
|
+ voice.gender === 'female' ? "bg-pink-500/20 text-pink-300" :
|
|
|
+ voice.gender === 'male' ? "bg-blue-500/20 text-blue-300" :
|
|
|
+ "bg-purple-500/20 text-purple-300"
|
|
|
+ )}>
|
|
|
+ {t(`aiConfig.voiceSettings.genders.${voice.gender}`, voice.gender)}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ {/* Play button */}
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ className={cn(
|
|
|
+ "h-7 w-7 p-0 rounded-full flex-shrink-0",
|
|
|
+ playingVoiceId === voice.id
|
|
|
+ ? "bg-cyan-500 text-white hover:bg-cyan-600"
|
|
|
+ : "bg-slate-600 text-slate-300 hover:bg-slate-500"
|
|
|
+ )}
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ handlePlayVoice(voice);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {playingVoiceId === voice.id ? (
|
|
|
+ <Pause className="w-3.5 h-3.5" />
|
|
|
+ ) : (
|
|
|
+ <Play className="w-3.5 h-3.5 ml-0.5" />
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ {/* Selection indicator */}
|
|
|
+ {aiConfig.voice_type === voice.provider_voice_id && (
|
|
|
+ <div className="w-2 h-2 rounded-full bg-cyan-500 flex-shrink-0" />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
|
|
|
+ {/* Conversation Behavior Card */}
|
|
|
<Card className="bg-slate-800 border-slate-700">
|
|
|
<CardHeader>
|
|
|
<div className="flex items-center gap-3">
|
|
|
@@ -224,54 +649,288 @@ export function AIConfigContent() {
|
|
|
<p className="text-slate-400">{t('aiConfig.conversationBehavior.subtitle')} {selectedShop.store_name || 'your store'} customers</p>
|
|
|
</CardHeader>
|
|
|
<CardContent className="space-y-6">
|
|
|
+ {/* Greeting Message */}
|
|
|
<div className="space-y-2">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.conversationBehavior.greetingMessage')}</Label>
|
|
|
- <Textarea
|
|
|
- className="bg-slate-700 border-slate-600 text-white min-h-[100px]"
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <Label className="text-slate-300">{t('aiConfig.conversationBehavior.greetingMessage')}</Label>
|
|
|
+ <span className="text-xs text-slate-400">{aiConfig.greeting_message.length}/255</span>
|
|
|
+ </div>
|
|
|
+ <Input
|
|
|
+ type="text"
|
|
|
+ className={cn(
|
|
|
+ "bg-slate-700 border-slate-600 text-white",
|
|
|
+ greetingError && "border-red-500"
|
|
|
+ )}
|
|
|
value={aiConfig.greeting_message}
|
|
|
- onChange={(e) => setAiConfig({ ...aiConfig, greeting_message: e.target.value })}
|
|
|
+ onChange={handleGreetingChange}
|
|
|
+ maxLength={255}
|
|
|
+ placeholder={t('aiConfig.conversationBehavior.greetingPlaceholder', 'Enter a greeting message...')}
|
|
|
/>
|
|
|
+ {greetingError && (
|
|
|
+ <p className="text-xs text-red-400">{greetingError}</p>
|
|
|
+ )}
|
|
|
+ <p className="text-xs text-slate-500">{t('aiConfig.conversationBehavior.greetingHint', 'No emails, URLs, or website addresses allowed')}</p>
|
|
|
</div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
|
|
|
- <div className="grid gap-6 md:grid-cols-2">
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <div className="space-y-1">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.conversationBehavior.businessHoursMode')}</Label>
|
|
|
- <p className="text-sm text-slate-400">{t('aiConfig.conversationBehavior.businessHoursModeDesc')}</p>
|
|
|
- </div>
|
|
|
- <Switch
|
|
|
- checked={aiConfig.business_hours_mode}
|
|
|
- onCheckedChange={(checked) => setAiConfig({ ...aiConfig, business_hours_mode: checked })}
|
|
|
- className="data-[state=checked]:bg-cyan-500"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <div className="space-y-1">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.conversationBehavior.localCurrencySupport')}</Label>
|
|
|
- <p className="text-sm text-slate-400">{t('aiConfig.conversationBehavior.localCurrencySupportDesc')}</p>
|
|
|
+ {/* Business Hours Card */}
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <Clock className="w-6 h-6 text-cyan-500" />
|
|
|
+ <div>
|
|
|
+ <CardTitle className="text-white">{t('aiConfig.businessHours.title', 'Business Hours')}</CardTitle>
|
|
|
+ <p className="text-slate-400 text-sm mt-1">{t('aiConfig.businessHours.subtitle', 'Set your store operating hours')}</p>
|
|
|
</div>
|
|
|
- <Switch
|
|
|
- checked={aiConfig.local_currency_support}
|
|
|
- onCheckedChange={(checked) => setAiConfig({ ...aiConfig, local_currency_support: checked })}
|
|
|
- className="data-[state=checked]:bg-cyan-500"
|
|
|
- />
|
|
|
</div>
|
|
|
+ <Switch
|
|
|
+ checked={businessHours.is_enabled}
|
|
|
+ onCheckedChange={(checked) => setBusinessHours({ ...businessHours, is_enabled: checked })}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
</div>
|
|
|
+ </CardHeader>
|
|
|
|
|
|
- <div className="space-y-2">
|
|
|
- <Label className="text-slate-300">{t('aiConfig.conversationBehavior.escalationPolicy')}</Label>
|
|
|
- <Select value={aiConfig.escalation_policy} onValueChange={(value) => setAiConfig({ ...aiConfig, escalation_policy: value })}>
|
|
|
- <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
|
|
- <SelectValue />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
- <SelectItem value="low">{t('aiConfig.conversationBehavior.escalationPolicies.low')}</SelectItem>
|
|
|
- <SelectItem value="medium">{t('aiConfig.conversationBehavior.escalationPolicies.medium')}</SelectItem>
|
|
|
- <SelectItem value="high">{t('aiConfig.conversationBehavior.escalationPolicies.high')}</SelectItem>
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- </div>
|
|
|
- </CardContent>
|
|
|
+ {businessHours.is_enabled && (
|
|
|
+ <CardContent className="space-y-6">
|
|
|
+ {businessHoursLoading ? (
|
|
|
+ <div className="flex items-center justify-center py-8">
|
|
|
+ <Loader2 className="w-6 h-6 animate-spin text-cyan-500" />
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ {/* Regular Hours - Per day */}
|
|
|
+ <div className="space-y-3">
|
|
|
+ <Label className="text-slate-300">{t('aiConfig.businessHours.regularHours', 'Regular Hours')}</Label>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ {WEEKDAYS.map((day) => {
|
|
|
+ const dayHours = businessHours.daily_hours[day as WeekDay];
|
|
|
+ return (
|
|
|
+ <div key={day} className="flex items-center gap-3 bg-slate-700/30 p-2 rounded-lg">
|
|
|
+ {/* Day name */}
|
|
|
+ <span className="text-white w-[100px] text-sm">
|
|
|
+ {t(`aiConfig.businessHours.days.${day}`, day.charAt(0).toUpperCase() + day.slice(1))}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ {/* Is Open Switch */}
|
|
|
+ <Switch
|
|
|
+ checked={dayHours.is_open}
|
|
|
+ onCheckedChange={(checked) => {
|
|
|
+ setBusinessHours({
|
|
|
+ ...businessHours,
|
|
|
+ daily_hours: {
|
|
|
+ ...businessHours.daily_hours,
|
|
|
+ [day]: { ...dayHours, is_open: checked }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
+
|
|
|
+ {dayHours.is_open ? (
|
|
|
+ <>
|
|
|
+ {/* Opening Time */}
|
|
|
+ <Select
|
|
|
+ value={dayHours.open}
|
|
|
+ onValueChange={(value) => {
|
|
|
+ setBusinessHours({
|
|
|
+ ...businessHours,
|
|
|
+ daily_hours: {
|
|
|
+ ...businessHours.daily_hours,
|
|
|
+ [day]: { ...dayHours, open: value }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
|
|
|
+ {TIME_OPTIONS.map((time) => (
|
|
|
+ <SelectItem key={time} value={time}>{time}</SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <span className="text-slate-400">-</span>
|
|
|
+ {/* Closing Time */}
|
|
|
+ <Select
|
|
|
+ value={dayHours.close}
|
|
|
+ onValueChange={(value) => {
|
|
|
+ setBusinessHours({
|
|
|
+ ...businessHours,
|
|
|
+ daily_hours: {
|
|
|
+ ...businessHours.daily_hours,
|
|
|
+ [day]: { ...dayHours, close: value }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
|
|
|
+ {TIME_OPTIONS.map((time) => (
|
|
|
+ <SelectItem key={time} value={time}>{time}</SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <span className="text-red-400 text-sm">{t('aiConfig.businessHours.closed', 'Closed')}</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Special Hours / Holidays */}
|
|
|
+ <div className="space-y-4 pt-4 border-t border-slate-700">
|
|
|
+ <Label className="text-slate-300">{t('aiConfig.businessHours.specialHours', 'Special Hours / Holidays')}</Label>
|
|
|
+
|
|
|
+ {/* Add Special Hours Form */}
|
|
|
+ <div className="flex items-center gap-2 flex-wrap bg-slate-700/50 p-3 rounded-lg">
|
|
|
+ {/* Date Picker */}
|
|
|
+ <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
|
|
|
+ <PopoverTrigger asChild>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ className={cn(
|
|
|
+ "w-[140px] justify-start text-left font-normal bg-slate-700 border-slate-600 text-white hover:bg-slate-600",
|
|
|
+ !newSpecialDate && "text-slate-400"
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <Calendar className="mr-2 h-4 w-4" />
|
|
|
+ {newSpecialDate ? format(newSpecialDate, 'yyyy-MM-dd') : t('aiConfig.businessHours.pickDate', 'Pick date')}
|
|
|
+ </Button>
|
|
|
+ </PopoverTrigger>
|
|
|
+ <PopoverContent className="w-auto p-0 bg-slate-800 border-slate-700" align="start">
|
|
|
+ <CalendarComponent
|
|
|
+ mode="single"
|
|
|
+ selected={newSpecialDate}
|
|
|
+ onSelect={(date) => {
|
|
|
+ setNewSpecialDate(date);
|
|
|
+ setCalendarOpen(false);
|
|
|
+ }}
|
|
|
+ disabled={(date) => date < new Date()}
|
|
|
+ initialFocus
|
|
|
+ locale={dateLocale}
|
|
|
+ className="bg-slate-800"
|
|
|
+ />
|
|
|
+ </PopoverContent>
|
|
|
+ </Popover>
|
|
|
+
|
|
|
+ {/* Note */}
|
|
|
+ <Input
|
|
|
+ type="text"
|
|
|
+ placeholder={t('aiConfig.businessHours.notePlaceholder', 'Note (e.g., Christmas)')}
|
|
|
+ value={newSpecialNote}
|
|
|
+ onChange={(e) => setNewSpecialNote(e.target.value)}
|
|
|
+ className="w-[160px] bg-slate-700 border-slate-600 text-white"
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* Closed Checkbox */}
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ id="isClosed"
|
|
|
+ checked={newSpecialIsClosed}
|
|
|
+ onChange={(e) => setNewSpecialIsClosed(e.target.checked)}
|
|
|
+ className="w-4 h-4 rounded border-slate-600 bg-slate-700 text-cyan-500 focus:ring-cyan-500"
|
|
|
+ />
|
|
|
+ <Label htmlFor="isClosed" className="text-slate-300 text-sm cursor-pointer">
|
|
|
+ {t('aiConfig.businessHours.closed', 'Closed')}
|
|
|
+ </Label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Time Range (only if not closed) */}
|
|
|
+ {!newSpecialIsClosed && (
|
|
|
+ <>
|
|
|
+ <Select
|
|
|
+ value={newSpecialOpeningTime}
|
|
|
+ onValueChange={setNewSpecialOpeningTime}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
|
|
|
+ {TIME_OPTIONS.map((time) => (
|
|
|
+ <SelectItem key={time} value={time}>{time}</SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <span className="text-slate-400">-</span>
|
|
|
+ <Select
|
|
|
+ value={newSpecialClosingTime}
|
|
|
+ onValueChange={setNewSpecialClosingTime}
|
|
|
+ >
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
|
|
|
+ {TIME_OPTIONS.map((time) => (
|
|
|
+ <SelectItem key={time} value={time}>{time}</SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Add Button */}
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
+ onClick={handleAddSpecialHours}
|
|
|
+ disabled={!newSpecialDate}
|
|
|
+ >
|
|
|
+ <Plus className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Special Hours List */}
|
|
|
+ {specialHours.length > 0 && (
|
|
|
+ <div className="space-y-2">
|
|
|
+ {specialHours.map((sh) => (
|
|
|
+ <div
|
|
|
+ key={sh.id}
|
|
|
+ className="flex items-center justify-between bg-slate-700/30 p-2 rounded-lg"
|
|
|
+ >
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <Calendar className="w-4 h-4 text-slate-400" />
|
|
|
+ <span className="text-white">{format(new Date(sh.date), 'yyyy-MM-dd')}</span>
|
|
|
+ {sh.is_closed ? (
|
|
|
+ <span className="text-red-400 text-sm">{t('aiConfig.businessHours.closed', 'Closed')}</span>
|
|
|
+ ) : (
|
|
|
+ <span className="text-slate-300 text-sm">{sh.opening_time} - {sh.closing_time}</span>
|
|
|
+ )}
|
|
|
+ {sh.note && (
|
|
|
+ <span className="text-slate-400 text-sm">({sh.note})</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ className="text-red-400 hover:text-red-300 hover:bg-red-500/20 h-8 w-8 p-0"
|
|
|
+ onClick={() => sh.id && handleDeleteSpecialHours(sh.id)}
|
|
|
+ >
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {specialHours.length === 0 && (
|
|
|
+ <p className="text-slate-500 text-sm">{t('aiConfig.businessHours.noSpecialHours', 'No special hours configured')}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ )}
|
|
|
</Card>
|
|
|
</div>
|
|
|
</div>
|