From 4fed69246fed3e53544bd0db249471f407efb686 Mon Sep 17 00:00:00 2001 From: ammarhamzi2019278344 Date: Tue, 27 May 2025 20:07:20 +0800 Subject: [PATCH 1/7] Implement AI-driven Asnaf analysis and UI updates --- pages/BF-PRF/AS/DETAIL/[id]/index.vue | 674 ++++++++++++++++++++++++++ pages/BF-PRF/AS/LIST/index.vue | 337 +++++++++++++ server/api/analyze-asnaf.post.ts | 124 +++++ 3 files changed, 1135 insertions(+) create mode 100644 pages/BF-PRF/AS/DETAIL/[id]/index.vue create mode 100644 pages/BF-PRF/AS/LIST/index.vue create mode 100644 server/api/analyze-asnaf.post.ts diff --git a/pages/BF-PRF/AS/DETAIL/[id]/index.vue b/pages/BF-PRF/AS/DETAIL/[id]/index.vue new file mode 100644 index 0000000..81c6af4 --- /dev/null +++ b/pages/BF-PRF/AS/DETAIL/[id]/index.vue @@ -0,0 +1,674 @@ + + + \ No newline at end of file diff --git a/pages/BF-PRF/AS/LIST/index.vue b/pages/BF-PRF/AS/LIST/index.vue new file mode 100644 index 0000000..00a279a --- /dev/null +++ b/pages/BF-PRF/AS/LIST/index.vue @@ -0,0 +1,337 @@ + + + \ No newline at end of file diff --git a/server/api/analyze-asnaf.post.ts b/server/api/analyze-asnaf.post.ts new file mode 100644 index 0000000..2034b0e --- /dev/null +++ b/server/api/analyze-asnaf.post.ts @@ -0,0 +1,124 @@ +import { defineEventHandler, readBody } from 'h3'; + +// Define an interface for the expected request body (subset of AsnafProfile) +interface AsnafAnalysisRequest { + monthlyIncome: string; + otherIncome: string; + totalIncome: string; + occupation: string; + maritalStatus: string; + dependents: Array; // Or a more specific type if you have one for dependents + // Add any other fields you deem necessary for OpenAI to analyze +} + +interface AidSuggestion { + nama: string; + peratusan: string; +} + +// Define an interface for the expected OpenAI response structure (and our API response) +interface AsnafAnalysisResponse { + hadKifayahPercentage: string; + kategoriAsnaf: string; + kategoriKeluarga: string; + cadanganKategori: string; + statusKelayakan: string; + cadanganBantuan: AidSuggestion[]; + ramalanJangkaMasaPulih: string; + rumusan: string; +} + +export default defineEventHandler(async (event): Promise => { + const body = await readBody(event); + + // --- Placeholder for Actual OpenAI API Call --- + // In a real application, you would: + // 1. Retrieve your OpenAI API key securely (e.g., from environment variables) + const openAIApiKey = process.env.OPENAI_API_KEY; + if (!openAIApiKey) { + console.error('OpenAI API key not configured. Please set OPENAI_API_KEY in your .env file.'); + throw createError({ statusCode: 500, statusMessage: 'OpenAI API key not configured' }); + } + + // 2. Construct the prompt for OpenAI using the data from `body`. + // IMPORTANT: Sanitize or carefully construct any data from `body` included in the prompt to prevent prompt injection. + const prompt = `You are an expert Zakat administrator. Based on the following applicant data: monthlyIncome: ${body.monthlyIncome}, totalIncome: ${body.totalIncome}, occupation: ${body.occupation}, maritalStatus: ${body.maritalStatus}, dependents: ${body.dependents.length}. +Return JSON with keys: hadKifayahPercentage, kategoriAsnaf, kategoriKeluarga, cadanganKategori, statusKelayakan, cadanganBantuan, ramalanJangkaMasaPulih, rumusan. +For 'cadanganBantuan', provide a JSON array of objects, where each object has a 'nama' (string, name of the aid) and 'peratusan' (string, e.g., '85%', representing suitability). Suggest 2-3 most relevant aid types. +Example for cadanganBantuan: [{"nama": "Bantuan Kewangan Bulanan", "peratusan": "90%"}, {"nama": "Bantuan Makanan Asas", "peratusan": "75%"}]. +Full JSON Example: {"hadKifayahPercentage": "75%", ..., "cadanganBantuan": [{"nama": "Bantuan Kewangan Bulanan", "peratusan": "90%"}], ...}`; + // Adjust the prompt to be more detailed and specific to your needs and desired JSON output structure. + + // 3. Make the API call to OpenAI + try { + const openAIResponse = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${openAIApiKey}`, + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', // Or your preferred model like gpt-4 + messages: [{ role: 'user', content: prompt }], + // For more consistent JSON output, consider using a model version that officially supports JSON mode if available + // and set response_format: { type: "json_object" }, (check OpenAI documentation for model compatibility) + }), + }); + + if (!openAIResponse.ok) { + const errorData = await openAIResponse.text(); + console.error('OpenAI API Error details:', errorData); + throw createError({ statusCode: openAIResponse.status, statusMessage: `Failed to get analysis from OpenAI: ${openAIResponse.statusText}` }); + } + + const openAIData = await openAIResponse.json(); + + // Parse the content from the response - structure might vary slightly based on OpenAI model/API version + // It's common for the JSON string to be in openAIData.choices[0].message.content + if (openAIData.choices && openAIData.choices[0] && openAIData.choices[0].message && openAIData.choices[0].message.content) { + const analysisResult = JSON.parse(openAIData.choices[0].message.content) as AsnafAnalysisResponse; + return analysisResult; + } else { + console.error('OpenAI response structure not as expected:', openAIData); + throw createError({ statusCode: 500, statusMessage: 'Unexpected response structure from OpenAI' }); + } + + } catch (error) { + console.error('Error during OpenAI API call or parsing:', error); + // Avoid exposing detailed internal errors to the client if they are not createError objects + if (typeof error === 'object' && error !== null && 'statusCode' in error) { + // We can infer error has statusCode here, but to be super safe with TS: + const e = error as { statusCode: number }; + if (e.statusCode) throw e; + } + throw createError({ statusCode: 500, statusMessage: 'Internal server error during AI analysis' }); + } + // --- End of Actual OpenAI API Call --- + + // The simulated response below this line should be REMOVED once the actual OpenAI call is implemented and working. + /* + console.log('Received for analysis in server route:', body); + await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API delay + + const totalIncomeNumeric = parseFloat(body.totalIncome); + let percentage = '50%'; + if (totalIncomeNumeric < 1000) percentage = '30%'; + else if (totalIncomeNumeric < 2000) percentage = '65%'; + else if (totalIncomeNumeric < 3000) percentage = '85%'; + else percentage = '110%'; + + return { + hadKifayahPercentage: percentage, + kategoriAsnaf: 'Simulated - Miskin', + kategoriKeluarga: 'Simulated - Miskin (50-100% HK)', + cadanganKategori: 'Simulated - Miskin', + statusKelayakan: 'Simulated - Layak (Miskin)', + cadanganBantuan: [ + { nama: 'Simulated - Bantuan Kewangan Bulanan', peratusan: '80%' }, + { nama: 'Simulated - Bantuan Pendidikan Anak', peratusan: '65%' } + ], + ramalanJangkaMasaPulih: 'Simulated - 6 bulan', + rumusan: 'Simulated - Pemohon memerlukan perhatian segera.' + }; + */ +}); \ No newline at end of file -- 2.47.2 From bcbf2f0958c220e86ca00a188c297ace86ea4ff4 Mon Sep 17 00:00:00 2001 From: Afiq Date: Fri, 30 May 2025 11:45:35 +0800 Subject: [PATCH 2/7] Enhance loading experience by implementing quick loading logo fetch during SSR, update loading logo and site name retrieval logic, and replace toast notifications with alert prompts in site settings management. Remove unused Metabase and Notes pages. --- components/Loading.vue | 41 ++++++++++++++-- navigation/index.js | 12 ----- pages/devtool/config/site-settings/index.vue | 38 +++++++-------- pages/metabase/index.vue | 49 -------------------- pages/notes/index.vue | 30 ------------ server/api/devtool/config/loading-logo.js | 44 ++++++++++++++++++ 6 files changed, 100 insertions(+), 114 deletions(-) delete mode 100644 pages/metabase/index.vue delete mode 100644 pages/notes/index.vue create mode 100644 server/api/devtool/config/loading-logo.js diff --git a/components/Loading.vue b/components/Loading.vue index 622fac5..1843a70 100644 --- a/components/Loading.vue +++ b/components/Loading.vue @@ -12,16 +12,49 @@ const refreshPage = () => { window.location.reload(true); }; +// Fast loading logo - fetch during SSR to prevent hydration flash +const { data: quickLoadingData } = await useLazyFetch("/api/devtool/config/loading-logo", { + default: () => ({ + data: { + siteLoadingLogo: '', + siteName: 'Loading...' + } + }), + transform: (response) => response.data || { + siteLoadingLogo: '', + siteName: 'Loading...' + } +}); + const loadingLogoSrc = computed(() => { - return 'http://localhost:3003/uploads/site-settings/loading-logo.png'; + // First priority: Quick loading data if available + if (quickLoadingData.value?.siteLoadingLogo) { + return quickLoadingData.value.siteLoadingLogo; + } + + // Second priority: Full site settings if loaded + if (!siteSettingsLoading.value && siteSettings.value.siteLoadingLogo) { + return siteSettings.value.siteLoadingLogo; + } + + // Fallback: Default logo + return '/img/logo/corradAF-logo.svg'; }); // Get site name with fallback const getSiteName = () => { - if (siteSettingsLoading.value) { - return 'Loading Logo'; + // First priority: Quick loading data + if (quickLoadingData.value?.siteName) { + return quickLoadingData.value.siteName; } - return siteSettings.value?.siteName || 'Loading Logo'; + + // Second priority: Full site settings + if (!siteSettingsLoading.value && siteSettings.value.siteName) { + return siteSettings.value.siteName; + } + + // Fallback + return 'Loading...'; }; diff --git a/navigation/index.js b/navigation/index.js index 9a543d0..ed66b1c 100644 --- a/navigation/index.js +++ b/navigation/index.js @@ -9,18 +9,6 @@ export default [ "icon": "ic:outline-dashboard", "child": [], "meta": {} - }, - { - "title": "Notes", - "path": "/notes", - "icon": "", - "child": [] - }, - { - "title": "Metabase", - "path": "/metabase", - "icon": "", - "child": [] } ], "meta": {} diff --git a/pages/devtool/config/site-settings/index.vue b/pages/devtool/config/site-settings/index.vue index d22b6e4..05f2139 100644 --- a/pages/devtool/config/site-settings/index.vue +++ b/pages/devtool/config/site-settings/index.vue @@ -5,7 +5,7 @@ definePageMeta({ requiresAuth: true, }); -const { $swal, $toast } = useNuxtApp(); +const { $swal } = useNuxtApp(); const { siteSettings, updateSiteSettings, applyThemeSettings, updateGlobalMeta } = useSiteSettings(); // Reactive data @@ -139,7 +139,7 @@ const loadSettings = async () => { } } catch (error) { console.error("Error loading settings:", error); - $toast.error("Failed to load site settings"); + alert("Failed to load site settings"); } finally { loading.value = false; } @@ -148,7 +148,7 @@ const loadSettings = async () => { // Save settings const saveSettings = async () => { if (!validateForm()) { - $toast.error("Please fix the validation errors"); + alert("Please fix the validation errors"); return; } @@ -160,7 +160,7 @@ const saveSettings = async () => { if (result && result.success) { originalSettings.value = { ...settings.value }; - $toast.success("Settings saved successfully"); + alert("Settings saved successfully"); // Apply changes // applyChanges(); // Temporarily commented out to isolate the error source @@ -175,7 +175,7 @@ const saveSettings = async () => { console.error("[SiteSettingsPage] 'result' from updateSiteSettings was undefined."); } - $toast.error(errorMsg); + alert(errorMsg); if (result && result.error && result.error.details) { console.error("[SiteSettingsPage] Save settings error details:", result.error.details); @@ -189,7 +189,7 @@ const saveSettings = async () => { // This catch block is for unexpected errors during the saveSettings execution itself, // or if updateSiteSettings somehow re-throws an error not caught by its own try-catch. console.error("Critical error saving settings:", error); - $toast.error("A critical error occurred. Failed to save settings."); + alert("A critical error occurred. Failed to save settings."); } finally { saving.value = false; } @@ -220,7 +220,7 @@ const applyFontFromSource = () => { } } - $toast.success('Font applied successfully'); + alert('Font applied successfully'); } }; @@ -241,7 +241,7 @@ const uploadFile = async (file, type) => { return response.data.url; } catch (error) { console.error(`Error uploading ${type}:`, error); - $toast.error(`Failed to upload ${type}`); + alert(`Failed to upload ${type}`); return null; } }; @@ -253,7 +253,7 @@ const handleLogoUpload = async (event) => { const url = await uploadFile(file, 'logo'); if (url) { settings.value.siteLogo = url; - $toast.success('Logo uploaded successfully'); + alert('Logo uploaded successfully'); } } }; @@ -264,7 +264,7 @@ const handleLoadingLogoUpload = async (event) => { const url = await uploadFile(file, 'loading-logo'); if (url) { settings.value.siteLoadingLogo = url; - $toast.success('Loading logo uploaded successfully'); + alert('Loading logo uploaded successfully'); } } }; @@ -275,7 +275,7 @@ const handleFaviconUpload = async (event) => { const url = await uploadFile(file, 'favicon'); if (url) { settings.value.siteFavicon = url; - $toast.success('Favicon uploaded successfully'); + alert('Favicon uploaded successfully'); } } }; @@ -286,7 +286,7 @@ const handleLoginLogoUpload = async (event) => { const url = await uploadFile(file, 'login-logo'); if (url) { settings.value.siteLoginLogo = url; - $toast.success('Login logo uploaded successfully'); + alert('Login logo uploaded successfully'); } } }; @@ -295,14 +295,14 @@ const handleCSSUpload = async (event) => { const file = event.target.files[0]; if (file) { if (!file.name.endsWith('.css')) { - $toast.error('Please upload a valid CSS file'); + alert('Please upload a valid CSS file'); return; } const reader = new FileReader(); reader.onload = (e) => { settings.value.customCSS = e.target.result; - $toast.success('CSS file loaded successfully'); + alert('CSS file loaded successfully'); }; reader.readAsText(file); } @@ -314,7 +314,7 @@ const handleOgImageUpload = async (event) => { const url = await uploadFile(file, 'og-image'); if (url) { settings.value.seoOgImage = url; - $toast.success('OG image uploaded successfully'); + alert('OG image uploaded successfully'); } } }; @@ -332,7 +332,7 @@ const resetSettings = () => { settings.value = { ...originalSettings.value }; errors.value = {}; applyChanges(); - $toast.info('Settings reset to last saved state'); + alert('Settings reset to last saved state'); }; // Check for changes @@ -364,7 +364,7 @@ const applyGoogleFont = (font) => { settings.value.fontSource = googleFontUrl; settings.value.currentFont = font.name; applyFontFromSource(); - $toast.success(`${font.name} font applied successfully`); + alert(`${font.name} font applied successfully`); // Reset the dropdown after selection selectedGoogleFont.value = ''; } @@ -451,7 +451,7 @@ watch(() => settings.value.showSiteNameInHeader, (newValue) => {
-