diff --git a/application/src/api/settings/index.ts b/application/src/api/settings/index.ts index c9d3cf7..58f85b2 100644 --- a/application/src/api/settings/index.ts +++ b/application/src/api/settings/index.ts @@ -1,69 +1,113 @@ - -import { settingsService } from "@/services/settingsService"; +import { pb, getCurrentEndpoint } from '@/lib/pocketbase'; const settingsApi = async (body: any) => { try { const { action, data } = body; - + console.log('Settings API called with action:', action, 'data:', data); + + const authToken = pb.authStore.token; + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + const baseUrl = getCurrentEndpoint(); + switch (action) { case 'getSettings': - const settings = await settingsService.getGeneralSettings(); - return { - status: 200, - json: { - success: true, - data: settings - } - }; - - case 'updateSettings': - if (!data.id) { + try { + const response = await fetch(`${baseUrl}/api/settings`, { + method: 'GET', + headers, + }); + + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const settings = await response.json(); return { - status: 400, - json: { - success: false, - message: 'Settings ID is required' - } + status: 200, + json: { success: true, data: settings }, + }; + } catch (error) { + console.error('Error fetching settings:', error); + return { + status: 500, + json: { success: false, message: 'Failed to fetch settings' }, }; } - - const updatedSettings = await settingsService.updateGeneralSettings(data.id, data); - return { - status: 200, - json: { - success: true, - data: updatedSettings + + case 'updateSettings': + try { + let response = await fetch(`${baseUrl}/api/settings`, { + method: 'PATCH', + headers, + body: JSON.stringify(data), + }); + + if (!response.ok && (response.status === 404 || response.status === 405)) { + response = await fetch(`${baseUrl}/api/settings`, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); } - }; - + + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const updatedSettings = await response.json(); + return { + status: 200, + json: { success: true, data: updatedSettings }, + }; + } catch (error) { + console.error('Error updating settings:', error); + return { + status: 500, + json: { success: false, message: 'Failed to update settings' }, + }; + } + case 'testEmailConnection': - // This would typically connect to the SMTP server to test the connection - // For now, we'll just simulate a successful connection - return { - status: 200, - json: { - success: true, - message: 'Connection successful' - } - }; - + try { + const response = await fetch(`${baseUrl}/api/settings/test-email`, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const result = await response.json(); + return { + status: 200, + json: { + success: result.success || false, + message: + result.message || (result.success ? 'Connection successful' : 'Connection failed'), + }, + }; + } catch (error) { + console.error('Error testing email connection:', error); + return { + status: 500, + json: { success: false, message: 'Failed to test email connection' }, + }; + } + default: return { status: 400, - json: { - success: false, - message: 'Invalid action' - } + json: { success: false, message: 'Invalid action' }, }; } } catch (error) { - console.error('Error in settings API:', error); + console.error('Unexpected error in settingsApi:', error); return { status: 500, - json: { - success: false, - message: 'Internal server error' - } + json: { success: false, message: 'Internal server error' }, }; } }; diff --git a/application/src/components/settings/GeneralSettings.tsx b/application/src/components/settings/GeneralSettings.tsx index 1aa3d86..96331ff 100644 --- a/application/src/components/settings/GeneralSettings.tsx +++ b/application/src/components/settings/GeneralSettings.tsx @@ -1,105 +1,4 @@ -import React, { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Settings } from "lucide-react"; -import { settingsService, type GeneralSettings } from "@/services/settingsService"; -import { useToast } from "@/hooks/use-toast"; +import GeneralSettingsPanel from './general/GeneralSettingsPanel'; -const GeneralSettingsPanel = () => { - const { toast } = useToast(); - const [formData, setFormData] = useState>({}); - const [isEditing, setIsEditing] = useState(false); - - const { data: settings, isLoading, error, refetch } = useQuery({ - queryKey: ['generalSettings'], - queryFn: settingsService.getGeneralSettings, - }); - - useEffect(() => { - if (settings) { - setFormData(settings); - } - }, [settings]); - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value - })); - }; - - const handleSave = async () => { - if (!settings?.id || !formData) return; - - try { - const result = await settingsService.updateGeneralSettings(settings.id, formData); - if (result) { - toast({ - title: "Settings updated", - description: "Your settings have been updated successfully.", - variant: "default", - }); - refetch(); - setIsEditing(false); - } - } catch (error) { - console.error("Error updating settings:", error); - toast({ - title: "Update failed", - description: "There was a problem updating your settings.", - variant: "destructive", - }); - } - }; - - if (isLoading) { - return
Loading settings...
; - } - - if (error) { - return
Error loading settings
; - } - - return ( -
- - - System Settings - - Configure your system settings and preferences - - - -
- - -
-
- - {isEditing ? ( - <> - - - - ) : ( - - )} - -
-
- ); -}; - -export default GeneralSettingsPanel; +export default GeneralSettingsPanel; \ No newline at end of file diff --git a/application/src/components/settings/general/GeneralSettingsPanel.tsx b/application/src/components/settings/general/GeneralSettingsPanel.tsx new file mode 100644 index 0000000..95e6a8a --- /dev/null +++ b/application/src/components/settings/general/GeneralSettingsPanel.tsx @@ -0,0 +1,216 @@ + +import React, { useState, useEffect } from 'react'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Settings, Mail } from "lucide-react"; +import { Form } from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { useSystemSettings } from "@/hooks/useSystemSettings"; +import { GeneralSettings } from "@/services/settingsService"; +import SystemSettingsTab from './SystemSettingsTab'; +import MailSettingsTab from './MailSettingsTab'; +import { GeneralSettingsPanelProps } from './types'; + +const GeneralSettingsPanel: React.FC = () => { + const { t } = useLanguage(); + const [isEditing, setIsEditing] = useState(false); + const [activeTab, setActiveTab] = useState("system"); + + const { + settings, + isLoading, + error, + updateSettings, + isUpdating, + testEmailConnection, + isTestingConnection + } = useSystemSettings(); + + const form = useForm({ + defaultValues: { + meta: { + appName: '', + appURL: '', + senderName: '', + senderAddress: '', + hideControls: false + }, + smtp: { + enabled: false, + port: 587, + host: '', + username: '', + authMethod: '', + tls: true, + localName: '' + } + } + }); + + useEffect(() => { + if (settings) { + // Initialize form with existing settings, using system_name for appName if meta.appName is not set + const appName = settings.meta?.appName || settings.system_name || ''; + + form.reset({ + ...settings, + meta: { + appName: appName, + appURL: settings.meta?.appURL || '', + senderName: settings.meta?.senderName || '', + senderAddress: settings.meta?.senderAddress || '', + hideControls: settings.meta?.hideControls || false + }, + smtp: settings.smtp || { + enabled: false, + port: 587, + host: '', + username: '', + authMethod: '', + tls: true, + localName: '' + } + }); + } + }, [settings, form]); + + const handleSave = async (formData: GeneralSettings) => { + try { + // Prepare data for PocketBase settings update (no ID needed) + const dataToSave = { + ...formData, + system_name: formData.meta?.appName || settings?.system_name + }; + + console.log('Saving settings data:', dataToSave); + await updateSettings(dataToSave); + setIsEditing(false); + } catch (error) { + console.error("Error updating settings:", error); + } + }; + + const handleTestConnection = async () => { + try { + const smtpConfig = form.getValues('smtp'); + await testEmailConnection(smtpConfig); + } catch (error) { + console.error("Error testing connection:", error); + } + }; + + const handleEditClick = () => { + console.log('Edit button clicked, setting isEditing to true'); + setIsEditing(true); + }; + + const handleCancelClick = () => { + console.log('Cancel button clicked, setting isEditing to false'); + setIsEditing(false); + // Reset form to original values + if (settings) { + const appName = settings.meta?.appName || settings.system_name || ''; + form.reset({ + ...settings, + meta: { + appName: appName, + appURL: settings.meta?.appURL || '', + senderName: settings.meta?.senderName || '', + senderAddress: settings.meta?.senderAddress || '', + hideControls: settings.meta?.hideControls || false + }, + smtp: settings.smtp || { + enabled: false, + port: 587, + host: '', + username: '', + authMethod: '', + tls: true, + localName: '' + } + }); + } + }; + + if (isLoading) { + return
Loading settings...
; + } + + if (error) { + return
Error loading settings
; + } + + return ( +
+ + + {t("generalSettings", "menu")} + + {t("monitorSSLCertificates", "ssl")} + + + +
+ + + + + + {t("systemSettings", "settings")} + + + + {t("mailSettings", "settings")} + + + + + + + + + + + + + {/* Save and Cancel buttons - only show when editing */} + {isEditing && ( +
+ + +
+ )} +
+ +
+ + {/* Edit button - only show when not editing and outside the form */} + {!isEditing && ( + + + + )} +
+
+ ); +}; + +export default GeneralSettingsPanel; \ No newline at end of file diff --git a/application/src/components/settings/general/MailSettingsTab.tsx b/application/src/components/settings/general/MailSettingsTab.tsx new file mode 100644 index 0000000..fe6349c --- /dev/null +++ b/application/src/components/settings/general/MailSettingsTab.tsx @@ -0,0 +1,217 @@ + +import React from 'react'; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { SettingsTabProps } from "./types"; + +interface MailSettingsTabProps extends SettingsTabProps { + handleTestConnection: () => Promise; + isTestingConnection: boolean; +} + +const MailSettingsTab: React.FC = ({ + form, + isEditing, + handleTestConnection, + isTestingConnection +}) => { + const { t } = useLanguage(); + + return ( +
+
+ ( + + {t("senderName", "settings")} + + + + + )} + /> +
+ +
+ ( + + {t("senderEmail", "settings")} + + + + + )} + /> +
+ +
+ ( + + + + + {t("smtpEnabled", "settings")} + + )} + /> +
+ +
+
+ ( + + {t("smtpHost", "settings")} + + + + + )} + /> +
+ +
+ ( + + {t("smtpPort", "settings")} + + field.onChange(parseInt(e.target.value) || 587)} + disabled={!isEditing || !form.watch('smtp.enabled')} + placeholder="587" + /> + + + )} + /> +
+ +
+ ( + + {t("smtpUsername", "settings")} + + + + + )} + /> +
+ +
+ ( + + {t("smtpAuthMethod", "settings")} + + + + + )} + /> +
+ +
+ ( + + + + + {t("enableTLS", "settings")} + + )} + /> +
+ +
+ ( + + {t("localName", "settings")} + + + + + )} + /> +
+ + {isEditing && form.watch('smtp.enabled') && ( +
+ +
+ )} +
+
+ ); +}; + +export default MailSettingsTab; \ No newline at end of file diff --git a/application/src/components/settings/general/SystemSettingsTab.tsx b/application/src/components/settings/general/SystemSettingsTab.tsx new file mode 100644 index 0000000..377d11e --- /dev/null +++ b/application/src/components/settings/general/SystemSettingsTab.tsx @@ -0,0 +1,130 @@ + +import React from 'react'; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { SettingsTabProps } from "./types"; + +const SystemSettingsTab: React.FC = ({ form, isEditing, settings }) => { + const { t } = useLanguage(); + + return ( +
+
+
+ ( + + + {t("appName", "settings")} * + + + + + + )} + /> +
+ +
+ ( + + + {t("appURL", "settings")} * + + + + + + )} + /> +
+
+ +
+ ( + + + {t("senderName", "settings")} + + + + + + )} + /> +
+ +
+ ( + + + {t("senderEmail", "settings")} + + + + + + )} + /> +
+ +
+ ( + + + + + + {t("hideControls", "settings")} + + + )} + /> +
+
+ ); +}; + +export default SystemSettingsTab; \ No newline at end of file diff --git a/application/src/components/settings/general/types.ts b/application/src/components/settings/general/types.ts new file mode 100644 index 0000000..4dfb11d --- /dev/null +++ b/application/src/components/settings/general/types.ts @@ -0,0 +1,12 @@ + +import { GeneralSettings } from "@/services/settingsService"; + +export interface SettingsTabProps { + form: any; + isEditing: boolean; + settings?: GeneralSettings; +} + +export interface GeneralSettingsPanelProps { + // No props needed for now, but we can add them in the future +} \ No newline at end of file diff --git a/application/src/hooks/useSystemSettings.tsx b/application/src/hooks/useSystemSettings.tsx index 8bc5045..8a08a10 100644 --- a/application/src/hooks/useSystemSettings.tsx +++ b/application/src/hooks/useSystemSettings.tsx @@ -1,8 +1,20 @@ -import { useQuery } from '@tanstack/react-query'; -import { settingsService } from "@/services/settingsService"; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from "@/components/ui/use-toast"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { GeneralSettings } from "@/services/settingsService"; + +interface ApiResponse { + success: boolean; + data?: GeneralSettings; + message?: string; +} export function useSystemSettings() { + const queryClient = useQueryClient(); + const { t } = useLanguage(); + + // Fetch settings from API const { data: settings, isLoading, @@ -10,7 +22,128 @@ export function useSystemSettings() { refetch } = useQuery({ queryKey: ['generalSettings'], - queryFn: settingsService.getGeneralSettings, + queryFn: async (): Promise => { + try { + console.log('Fetching settings from API...'); + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'getSettings' }) + }); + + console.log('API response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result: ApiResponse = await response.json(); + console.log('API response data:', result); + + if (!result.success) { + throw new Error(result.message || 'Failed to fetch settings'); + } + + return result.data || null; + } catch (error) { + console.error('Error fetching settings:', error); + toast({ + title: t("errorFetchingSettings", "settings"), + description: error instanceof Error ? error.message : String(error), + variant: "destructive", + }); + return null; + } + } + }); + + // Update settings mutation + const updateSettingsMutation = useMutation({ + mutationFn: async (updatedSettings: GeneralSettings): Promise => { + console.log('Updating settings:', updatedSettings); + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + action: 'updateSettings', + data: updatedSettings + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result: ApiResponse = await response.json(); + + if (!result.success || !result.data) { + throw new Error(result.message || 'Failed to update settings'); + } + + return result.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['generalSettings'] }); + toast({ + title: t("settingsUpdated", "settings"), + description: "", + variant: "default", + }); + }, + onError: (error) => { + console.error('Error updating settings:', error); + toast({ + title: t("errorSavingSettings", "settings"), + description: error instanceof Error ? error.message : String(error), + variant: "destructive", + }); + } + }); + + // Test email connection + const testEmailConnectionMutation = useMutation({ + mutationFn: async (smtpConfig: any): Promise<{success: boolean, message: string}> => { + console.log('Testing email connection:', smtpConfig); + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + action: 'testEmailConnection', + data: smtpConfig + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return { + success: result.success, + message: result.message || '' + }; + }, + onSuccess: (result) => { + toast({ + title: result.success ? t("connectionSuccess", "settings") : t("connectionFailed", "settings"), + description: result.message, + variant: result.success ? "default" : "destructive", + }); + }, + onError: (error) => { + console.error('Error testing connection:', error); + toast({ + title: t("connectionFailed", "settings"), + description: error instanceof Error ? error.message : String(error), + variant: "destructive", + }); + } }); return { @@ -18,6 +151,10 @@ export function useSystemSettings() { isLoading, error, refetch, - systemName: settings?.system_name || 'ReamStack', + updateSettings: updateSettingsMutation.mutate, + isUpdating: updateSettingsMutation.isPending, + testEmailConnection: testEmailConnectionMutation.mutate, + isTestingConnection: testEmailConnectionMutation.isPending, + systemName: settings?.system_name || settings?.meta?.appName || 'ReamStack', }; -} +} \ No newline at end of file diff --git a/application/src/services/settingsService.ts b/application/src/services/settingsService.ts index b6f88ea..4fef9a4 100644 --- a/application/src/services/settingsService.ts +++ b/application/src/services/settingsService.ts @@ -1,11 +1,9 @@ -import { pb } from "@/lib/pocketbase"; - export interface GeneralSettings { - id: string; - created: string; - updated: string; - system_name: string; + id?: string; + created?: string; + updated?: string; + system_name?: string; system_name_kh?: string; logo_url?: string; system_description?: string; @@ -47,22 +45,55 @@ export interface GeneralSettings { export const settingsService = { async getGeneralSettings(): Promise { try { - const result = await pb.collection('general_settings').getList(1, 1); - if (result.items.length > 0) { - // Type cast to GeneralSettings to resolve the type mismatch - return result.items[0] as unknown as GeneralSettings; + console.log('Fetching settings from /api/settings endpoint...'); + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ action: 'getSettings' }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - return null; + + const result = await response.json(); + console.log('Settings API response:', result); + + return result.success ? result.data : null; } catch (error) { console.error("Failed to fetch general settings:", error); return null; } }, - async updateGeneralSettings(id: string, data: Partial): Promise { + async updateGeneralSettings(data: Partial): Promise { try { - // Type cast to GeneralSettings to resolve the type mismatch - return await pb.collection('general_settings').update(id, data) as unknown as GeneralSettings; + console.log('Updating settings via /api/settings:', data); + + // Remove id and timestamp fields for settings update + const { id, created, updated, ...updateData } = data; + + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'updateSettings', + data: updateData + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('Settings update response:', result); + + return result.success ? result.data : null; } catch (error) { console.error("Failed to update general settings:", error); return null; @@ -71,9 +102,24 @@ export const settingsService = { async testEmailConnection(smtpConfig: any): Promise { try { - // In a real application, we would send a test email here - // For now, let's simulate a successful connection if the host is provided - return !!smtpConfig.host; + console.log('Testing email connection via /api/settings:', smtpConfig); + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'testEmailConnection', + data: smtpConfig + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result.success || false; } catch (error) { console.error("Failed to test email connection:", error); return false;