diff --git a/app/dashboard/manajemen-posyandu/review/[id]/AdminReviewList.tsx b/app/dashboard/manajemen-posyandu/review/[id]/AdminReviewList.tsx new file mode 100644 index 0000000..31c1139 --- /dev/null +++ b/app/dashboard/manajemen-posyandu/review/[id]/AdminReviewList.tsx @@ -0,0 +1,114 @@ +'use client' + +import { useState } from 'react' +import { Star, Trash2, Calendar, User, MessageCircle } from 'lucide-react' +import { deleteReview } from './action-admin-review' +import { showSwal } from '@/lib/swal' + +interface Review { + id: string + rating: number + ulasan: string + nama_pengulas: string + created_at: string +} + +interface Props { + posyanduId: string + initialReviews: Review[] +} + +export function AdminReviewList({ posyanduId, initialReviews }: Props) { + const [isDeleting, setIsDeleting] = useState(null) + + const handleDelete = async (reviewId: string) => { + const result = await showSwal.confirm( + 'Hapus Ulasan?', + 'Apakah Anda yakin ingin menghapus ulasan ini? Tindakan ini tidak dapat dibatalkan.' + ) + + if (!result.isConfirmed) return + + setIsDeleting(reviewId) + try { + const res = await deleteReview(reviewId, posyanduId) + if (res.success) { + showSwal.success('Berhasil!', 'Ulasan telah dihapus.') + } else { + showSwal.error('Gagal!', res.error || 'Gagal menghapus ulasan.') + } + } catch (err) { + showSwal.error('Gagal!', 'Terjadi kesalahan sistem.') + } finally { + setIsDeleting(null) + } + } + + return ( +
+
+

+ + Ulasan Masyarakat +

+ + {initialReviews.length} TOTAL + +
+
+ {initialReviews.length > 0 ? ( + initialReviews.map((rev) => ( +
+
+
+
+ {rev.nama_pengulas?.[0] || 'A'} +
+
+

{rev.nama_pengulas || 'Orang Tua'}

+
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+
+
+ + {new Date(rev.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })} +
+
+
+
+ +
+
+

+ "{rev.ulasan}" +

+
+
+ )) + ) : ( +
+ +
+

Belum Ada Ulasan

+

Posyandu ini belum memiliki riwayat ulasan dari masyarakat.

+
+
+ )} +
+
+ ) +} diff --git a/app/dashboard/manajemen-posyandu/review/[id]/action-admin-review.ts b/app/dashboard/manajemen-posyandu/review/[id]/action-admin-review.ts new file mode 100644 index 0000000..c9cb5c0 --- /dev/null +++ b/app/dashboard/manajemen-posyandu/review/[id]/action-admin-review.ts @@ -0,0 +1,19 @@ +'use server' + +import { supabase } from '@/lib/supabase' +import { revalidatePath } from 'next/cache' + +export async function deleteReview(reviewId: string, posyanduId: string) { + const { error } = await supabase + .from('ulasan_posyandu') + .delete() + .eq('id', reviewId) + + if (error) { + console.error('Error deleting review:', error) + return { success: false, error: error.message } + } + + revalidatePath(`/dashboard/manajemen-posyandu/review/${posyanduId}`) + return { success: true } +} diff --git a/app/dashboard/manajemen-posyandu/review/[id]/page.tsx b/app/dashboard/manajemen-posyandu/review/[id]/page.tsx index 64e2325..0d4d2c8 100644 --- a/app/dashboard/manajemen-posyandu/review/[id]/page.tsx +++ b/app/dashboard/manajemen-posyandu/review/[id]/page.tsx @@ -2,8 +2,9 @@ import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { supabase } from '@/lib/supabase' import { LogoutButton } from '@/components/logout-button' -import { ArrowLeft, Building2, MapPin, Phone, User, ExternalLink, Calendar, Map as MapIcon } from 'lucide-react' +import { ArrowLeft, Building2, MapPin, User, ExternalLink, Map as MapIcon } from 'lucide-react' import Link from 'next/link' +import { AdminReviewList } from './AdminReviewList' interface Props { params: Promise<{ id: string }> @@ -22,7 +23,8 @@ export default async function ReviewPosyanduPage({ params }: Props) { .from('detail_posyandu') .select(` *, - petugas:petugas_posyandu_lokal(*) + petugas:petugas_posyandu_lokal(*), + reviews:ulasan_posyandu(*) `) .eq('id', id) .single() @@ -89,7 +91,7 @@ export default async function ReviewPosyanduPage({ params }: Props) {

- Daftarkan Petugas Bertugas + Daftar Petugas Terdaftar

{posyandu.petugas?.length || 0} Orang @@ -123,6 +125,12 @@ export default async function ReviewPosyanduPage({ params }: Props) { )}
+ + {/* Admin Review List Card */} + {/* Right Column - Map & Quick Sync */} diff --git a/app/user-dashboard/artikel/articles.json b/app/user-dashboard/artikel/articles.json new file mode 100644 index 0000000..86c7f85 --- /dev/null +++ b/app/user-dashboard/artikel/articles.json @@ -0,0 +1,92 @@ +[ + { + "id": "1", + "title": "Stunting: Apa, Penyebab, dan Upaya Penanganannya", + "category": "Edukasi Dasar", + "readTime": "5 min", + "source": "Kemenkes RI", + "description": "Pelajari definisi stunting, penyebab utama seperti malnutrisi kronis, serta langkah-langkah penanganan yang efektif.", + "url": "https://kesmas.kemkes.go.id" + }, + { + "id": "2", + "title": "Pemahaman Orang Tua: Kunci Mencegah Stunting", + "category": "Pola Asuh", + "readTime": "4 min", + "source": "Antara News", + "description": "Menkes menekankan bahwa pemahaman orang tua mengenai nutrisi adalah faktor terpenting dalam mencegah gagal tumbuh pada anak.", + "url": "https://www.antaranews.com" + }, + { + "id": "3", + "title": "Panduan Gizi Kemenkes untuk Balita", + "category": "Gizi & Nutrisi", + "readTime": "6 min", + "source": "Kemenkes RI", + "description": "Panduan terbaru mengenai asupan gizi seimbang, pentingnya protein hewani, dan suplementasi untuk balita.", + "url": "https://kemkes.go.id" + }, + { + "id": "4", + "title": "11 Program Intervensi Stunting Pemerintah", + "category": "Program Pemerintah", + "readTime": "7 min", + "source": "Kemenkes RI", + "description": "Mengenal program pemerintah mulai dari pemberian TTD bagi remaja putri hingga imunisasi lengkap untuk bayi.", + "url": "https://kemkes.go.id" + }, + { + "id": "5", + "title": "Peran Ibu dalam Mencegah Stunting dan Obesitas", + "category": "Keluarga", + "readTime": "5 min", + "source": "Promkes Kemenkes", + "description": "Diskusi mendalam mengenai peran krusial ibu dalam memantau berat badan selama kehamilan dan pemberian ASI eksklusif.", + "url": "https://promkes.kemkes.go.id" + }, + { + "id": "6", + "title": "Masalah Status Gizi Balita di Indonesia", + "category": "Edukasi Dasar", + "readTime": "6 min", + "source": "Kemenkes RI", + "description": "Ulasan mengenai faktor langsung dan tidak langsung yang mempengaruhi status gizi balita di masa emas pertumbuhan.", + "url": "https://kemkes.go.id" + }, + { + "id": "7", + "title": "Pentingnya Gizi Seimbang untuk Pertumbuhan Balita", + "category": "Gizi & Nutrisi", + "readTime": "4 min", + "source": "Poltekkes Makassar", + "description": "Mengapa gizi seimbang di awal kehidupan menjadi fondasi kesehatan, sistem imun, dan kecerdasan anak di masa depan.", + "url": "https://poltekkes-mks.ac.id" + }, + { + "id": "8", + "title": "Ayo ke Posyandu: Deteksi Dini Stunting", + "category": "Layanan Kesehatan", + "readTime": "3 min", + "source": "Stunting.go.id", + "description": "Manfaat rutin menimbang berat badan dan memantau perkembangan anak di Posyandu sebagai langkah deteksi dini.", + "url": "https://stunting.go.id" + }, + { + "id": "9", + "title": "Cegah Stunting itu Penting: Gerakan #AksiBergizi", + "category": "Kampanye", + "readTime": "4 min", + "source": "Ayo Sehat Kemenkes", + "description": "Informasi mengenai kampanye nasional pencegahan stunting melalui gerakan Aksi Bergizi dan Posyandu Aktif.", + "url": "https://ayosehat.kemkes.go.id" + }, + { + "id": "10", + "title": "Langkah Praktis Mencegah Stunting di Rumah", + "category": "Panduan Praktis", + "readTime": "5 min", + "source": "Ayo Sehat Kemenkes", + "description": "Tips harian bagi orang tua mulai dari stimulasi anak, menjaga kebersihan lingkungan, hingga pemenuhan gizi harian.", + "url": "https://ayosehat.kemkes.go.id" + } +] \ No newline at end of file diff --git a/app/user-dashboard/artikel/page.tsx b/app/user-dashboard/artikel/page.tsx new file mode 100644 index 0000000..37cf582 --- /dev/null +++ b/app/user-dashboard/artikel/page.tsx @@ -0,0 +1,108 @@ +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' +import { LogoutButton } from '@/components/logout-button' +import { ArrowLeft, BookOpen, Clock, Tag, ExternalLink, Search } from 'lucide-react' +import Link from 'next/link' +import articles from './articles.json' + +export default async function ArtikelStuntingPage() { + const cookieStore = await cookies() + const sessionCookie = cookieStore.get('user_session') + if (!sessionCookie) redirect('/') + + const session = JSON.parse(sessionCookie.value) + if (session.role !== 'user') redirect('/dashboard') + + return ( +
+ {/* Header */} +
+
+ +
+ +
+ Kembali + +
+
+

Edukasi Stunting

+

INFORMASI TERPERCAYA

+
+
+ +
+ +
+ {/* Hero Section */} +
+
+
+ Pencegahan Stunting +
+

+ Cegah Stunting
Mulai dari Sekarang +

+

+ Kumpulan informasi dan panduan teruji dari para ahli untuk membantu Bapak/Ibu memastikan tumbuh kembang optimal sang buah hati. +

+
+
+ + {articles.length} Artikel Terpilih +
+
+ + Update Berkala +
+
+
+ {/* Background Elements */} +
+
+
+
+ + {/* Article Grid */} +
+ {articles.map((article) => ( +
+
+
+
+ + {article.category} +
+
+ + {article.readTime} Baca +
+
+
+

+ {article.title} +

+

+ {article.description} +

+
+
+
+ SUMBER: {article.source} + + BACA LENGKAP + + +
+
+ ))} +
+
+
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/PosyanduCard.tsx b/app/user-dashboard/lokasi-posyandu/PosyanduCard.tsx new file mode 100644 index 0000000..b6eb0c3 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/PosyanduCard.tsx @@ -0,0 +1,83 @@ +'use client' + +import { MapPin, Phone, Eye, Building2, Star } from 'lucide-react' +import Link from 'next/link' + +interface Posyandu { + id: string + nama_posyandu: string + alamat: string + kontak: string | null + latitude: number | null + longitude: number | null + link_google_maps: string | null + petugas?: { + nama_petugas: string + nomor_hp: string | null + jabatan: string | null + }[] + reviews?: { + rating: number + ulasan: string + nama_pengulas: string + }[] +} + +interface Props { + data: Posyandu +} + +export default function PosyanduCard({ data }: Props) { + return ( +
+
+
+ +
+ {data.reviews && data.reviews.length > 0 && ( +
+ + + {(data.reviews.reduce((acc, r) => acc + r.rating, 0) / data.reviews.length).toFixed(1)} + + ({data.reviews.length}) +
+ )} +
+ +
+
+

+ {data.nama_posyandu} +

+
+ + {data.alamat} +
+
+ +
+
+ {data.petugas?.length || 0} Petugas +
+ {data.kontak && ( +
+ + {data.kontak} +
+ )} +
+
+ +
+ + + LIHAT DETAIL & REVIEW + +
+
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/PosyanduList.tsx b/app/user-dashboard/lokasi-posyandu/PosyanduList.tsx new file mode 100644 index 0000000..b1a91e1 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/PosyanduList.tsx @@ -0,0 +1,70 @@ +'use client' + +import { useState, useMemo } from 'react' +import { Search, Building2 } from 'lucide-react' +import PosyanduCard from './PosyanduCard' + +interface Posyandu { + id: string + nama_posyandu: string + alamat: string + kontak: string | null + latitude: number | null + longitude: number | null + link_google_maps: string | null + petugas?: { + nama_petugas: string + nomor_hp: string | null + jabatan: string | null + }[] + reviews?: { + rating: number + ulasan: string + nama_pengulas: string + }[] +} + +interface Props { + initialData: Posyandu[] +} + +export default function PosyanduList({ initialData }: Props) { + const [searchTerm, setSearchTerm] = useState('') + + const filteredData = useMemo(() => { + return initialData.filter(p => + p.nama_posyandu.toLowerCase().includes(searchTerm.toLowerCase()) || + p.alamat.toLowerCase().includes(searchTerm.toLowerCase()) + ) + }, [initialData, searchTerm]) + + return ( +
+ {/* Search Bar */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border-2 border-gray-100 focus:border-black rounded-xl text-sm font-semibold outline-none transition-all" + /> +
+ + {/* Grid of Cards */} + {filteredData.length === 0 ? ( +
+ +

Tidak ada posyandu ditemukan

+
+ ) : ( +
+ {filteredData.map((p) => ( + + ))} +
+ )} +
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/[id]/PosyanduDetailClient.tsx b/app/user-dashboard/lokasi-posyandu/[id]/PosyanduDetailClient.tsx new file mode 100644 index 0000000..a44b110 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/[id]/PosyanduDetailClient.tsx @@ -0,0 +1,270 @@ +'use client' + +import { useState, useEffect } from 'react' +import { MapPin, Phone, User, ExternalLink, Map as MapIcon, Star, Send, Loader2, Building2 } from 'lucide-react' +import { supabase } from '@/lib/supabase' +import { submitReview } from '../action-review' +import { showSwal } from '@/lib/swal' + +interface Posyandu { + id: string + nama_posyandu: string + alamat: string + kontak: string | null + latitude: number | null + longitude: number | null + link_google_maps: string | null + petugas?: { + nama_petugas: string + nomor_hp: string | null + jabatan: string | null + }[] +} + +interface Review { + id: string + rating: number + ulasan: string + nama_pengulas: string + created_at: string +} + +interface Props { + data: Posyandu + userId: string +} + +export default function PosyanduDetailClient({ data, userId }: Props) { + const [reviews, setReviews] = useState([]) + const [isLoadingReviews, setIsLoadingReviews] = useState(true) + const [rating, setRating] = useState(5) + const [comment, setComment] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + useEffect(() => { + fetchReviews() + }, [data.id]) + + async function fetchReviews() { + setIsLoadingReviews(true) + const { data: reviewData, error } = await supabase + .from('ulasan_posyandu') + .select('*') + .eq('posyandu_id', data.id) + .order('created_at', { ascending: false }) + + if (!error && reviewData) { + setReviews(reviewData as any) + } + setIsLoadingReviews(false) + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!comment.trim()) { + showSwal.error('Ops!', 'Komentar tidak boleh kosong.') + return + } + + setIsSubmitting(true) + const result = await submitReview({ + posyandu_id: data.id, + nama_pengulas: 'Orang Tua', // Or fetch the actual name if available in session + rating, + comment: comment + }) + + if (result.success) { + showSwal.success('Berhasil!', 'Review Anda telah terkirim.') + setComment('') + setRating(5) + fetchReviews() + } else { + showSwal.error('Gagal!', result.error || 'Terjadi kesalahan saat mengirim ulasan.') + } + setIsSubmitting(false) + } + + const mapSrc = `https://maps.google.com/maps?q=${encodeURIComponent(data.alamat)}&z=15&output=embed` + + return ( +
+ {/* Left Column - Information */} +
+ {/* Basic Info Card */} +
+
+

+ + Informasi Utama +

+
+
+
+ + + Alamat Posyandu + +

{data.alamat}

+
+ {data.kontak && ( +
+ + + Kontak Layanan + +

{data.kontak}

+
+ )} +
+
+ + {/* Petugas Card */} +
+
+

+ + Petugas Bertugas +

+ + {data.petugas?.length || 0} Orang + +
+
+ {data.petugas && data.petugas.length > 0 ? ( + data.petugas.map((pt: any, i) => ( +
+
+
+ {pt.nama_petugas?.[0]} +
+
+

{pt.nama_petugas}

+

{pt.jabatan || 'Anggota'}

+
+
+ {pt.nomor_hp && ( +
+ + {pt.nomor_hp} +
+ )} +
+ )) + ) : ( +
+ Belum ada data petugas terdaftar di posyandu ini. +
+ )} +
+
+ + {/* Reviews List */} +
+

+ + + Ulasan Pengguna + + {reviews.length} REVIEW +

+
+ {isLoadingReviews ? ( +
+ +

Memuat ulasan masyarakat...

+
+ ) : reviews.length > 0 ? ( + reviews.map((rev) => ( +
+
+
+
+ {rev.nama_pengulas?.[0] || 'A'} +
+

{rev.nama_pengulas || 'Orang Tua'}

+
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+
+

"{rev.ulasan}"

+

{new Date(rev.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })}

+
+ )) + ) : ( +
+ +

Belum ada ulasan untuk lokasi ini.

+
+ )} +
+
+
+ + {/* Right Column - Map & Review Form */} +
+ {/* Map Card */} +
+
+

+ + Lokasi Maps +

+ {data.link_google_maps && ( + + + + )} +
+
+ +
+
+ + {/* Review Form Card */} +
+
+

Berikan Ulasan

+

Ceritakan pengalaman Bapak/Ibu mengenai pelayanan di posyandu ini.

+ +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ + + + +
+
+ {/* Background decoration */} +
+
+
+
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/[id]/page.tsx b/app/user-dashboard/lokasi-posyandu/[id]/page.tsx new file mode 100644 index 0000000..37ead55 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/[id]/page.tsx @@ -0,0 +1,67 @@ +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' +import { supabase } from '@/lib/supabase' +import { LogoutButton } from '@/components/logout-button' +import { ArrowLeft, Building2, MapPin, Phone, User, ExternalLink, Map as MapIcon } from 'lucide-react' +import Link from 'next/link' +import PosyanduDetailClient from "./PosyanduDetailClient" + +interface Props { + params: Promise<{ id: string }> +} + +export default async function PosyanduDetailPage({ params }: Props) { + const { id } = await params + const cookieStore = await cookies() + const sessionCookie = cookieStore.get('user_session') + if (!sessionCookie) redirect('/') + + const session = JSON.parse(sessionCookie.value) + if (session.role !== 'user') redirect('/dashboard') + + const { data: posyandu, error } = await supabase + .from('detail_posyandu') + .select(` + *, + petugas:petugas_posyandu_lokal(*) + `) + .eq('id', id) + .single() + + if (error || !posyandu) { + return ( +
+
+

Data Tidak Ditemukan

+ Kembali ke Daftar Posyandu +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +
+ +
+ Kembali + +
+
+

{posyandu.nama_posyandu}

+

DETAIL LOKASI & ULASAN

+
+
+ +
+ +
+ +
+
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/action-review.ts b/app/user-dashboard/lokasi-posyandu/action-review.ts new file mode 100644 index 0000000..7622db9 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/action-review.ts @@ -0,0 +1,52 @@ +'use server' + +import { supabase } from '@/lib/supabase' +import { revalidatePath } from 'next/cache' + +export async function getPosyanduWithReviews() { + const { data: posyandu, error } = await supabase + .from('detail_posyandu') + .select(` + *, + petugas:petugas_posyandu_lokal(*), + reviews:ulasan_posyandu( + *, + nama_pengulas + ) + `) + .order('nama_posyandu', { ascending: true }) + + if (error) { + console.error('Error fetching posyandu:', error) + return [] + } + + return posyandu +} + +export async function submitReview(formData: { + posyandu_id: string + nama_pengulas: string + rating: number + comment: string +}) { + const { error } = await supabase + .from('ulasan_posyandu') + .insert([ + { + posyandu_id: formData.posyandu_id, + rating: formData.rating, + ulasan: formData.comment, + nama_pengulas: formData.nama_pengulas, // Add this field to the formData or handle it separately + created_at: new Date().toISOString() + } + ]) + + if (error) { + console.error('Error submitting review:', error) + return { success: false, error: error.message } + } + + revalidatePath('/user-dashboard/lokasi-posyandu') + return { success: true } +} diff --git a/app/user-dashboard/lokasi-posyandu/page.tsx b/app/user-dashboard/lokasi-posyandu/page.tsx new file mode 100644 index 0000000..2b562ea --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/page.tsx @@ -0,0 +1,96 @@ +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' +import { supabase } from '@/lib/supabase' +import { LogoutButton } from '@/components/logout-button' +import { ArrowLeft, Building2, Search } from 'lucide-react' +import Link from 'next/link' +import PosyanduList from "./PosyanduList" + +export async function getPosyanduWithReviews() { + const { data: posyandu, error } = await supabase + .from('detail_posyandu') + .select(` + *, + petugas:petugas_posyandu_lokal(*), + reviews:ulasan_posyandu( + rating, + ulasan, + nama_pengulas, + created_at + ) + `) + .order('nama_posyandu', { ascending: true }) + + if (error) { + console.error('Error fetching posyandu:', error) + return [] + } + + return posyandu +} + +export default async function LokasiPosyanduPage() { + const cookieStore = await cookies() + const sessionCookie = cookieStore.get('user_session') + if (!sessionCookie) redirect('/') + + const session = JSON.parse(sessionCookie.value) + if (session.role !== 'user') redirect('/dashboard') + + // Fetch all posyandu data using server action + const posyanduData = await getPosyanduWithReviews() + + if (!posyanduData) { + return ( +
+
+

Gagal Memuat Data

+

Terjadi kesalahan pada server.

+ Kembali ke Dashboard +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +
+ +
+ Kembali + +
+
+

Lokasi Posyandu

+

LAYANAN KESEHATAN

+
+
+ +
+ +
+ {/* Hero Section */} +
+
+

Temukan Layanan Terdekat

+

+ Cari lokasi posyandu di wilayah Anda, cek petugas yang bertugas, dan berikan ulasan untuk meningkatkan kualitas layanan. +

+
+
+ +
+
+
+ + {/* Client Component for Search and Cards */} + + +
+
+ ) +} diff --git a/app/user-dashboard/lokasi-posyandu/supabase-review-query.txt b/app/user-dashboard/lokasi-posyandu/supabase-review-query.txt new file mode 100644 index 0000000..74ed584 --- /dev/null +++ b/app/user-dashboard/lokasi-posyandu/supabase-review-query.txt @@ -0,0 +1,40 @@ +-- Query Supabase untuk Tabel Ulasan (Reviews) Posyandu + +-- 0. Aktifkan Ekstensi UUID (Jika belum aktif) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 1. Buat Tabel posyandu_reviews +CREATE TABLE IF NOT EXISTS posyandu_reviews ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + posyandu_id UUID REFERENCES detail_posyandu(id) ON DELETE CASCADE, + user_id UUID REFERENCES akun_balita(id) ON DELETE CASCADE, + rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), + comment TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. Aktifkan Row Level Security (RLS) +ALTER TABLE posyandu_reviews ENABLE ROW LEVEL SECURITY; + +-- 3. Kebijakan RLS (Policies) + +-- Kebijakan: Semua orang dapat melihat ulasan +CREATE POLICY "Anyone can view reviews" +ON posyandu_reviews FOR SELECT +USING (true); + +-- Kebijakan: User terautentikasi dapat mengirim ulasan (untuk akun_balita) +CREATE POLICY "Authenticated users can insert reviews" +ON posyandu_reviews FOR INSERT +TO authenticated +WITH CHECK (auth.uid() = user_id); + +-- Kebijakan: User dapat menghapus ulasan mereka sendiri +CREATE POLICY "Users can delete their own reviews" +ON posyandu_reviews FOR DELETE +TO authenticated +USING (auth.uid() = user_id); + +-- 4. Indeks untuk Performa +CREATE INDEX IF NOT EXISTS idx_posyandu_reviews_posyandu_id ON posyandu_reviews(posyandu_id); +CREATE INDEX IF NOT EXISTS idx_posyandu_reviews_user_id ON posyandu_reviews(user_id); diff --git a/app/user-dashboard/page.tsx b/app/user-dashboard/page.tsx index 8abd777..2a70ae4 100644 --- a/app/user-dashboard/page.tsx +++ b/app/user-dashboard/page.tsx @@ -201,7 +201,7 @@ export default async function UserDashboardPage() { title="Lokasi Posyandu" description="Temukan lokasi posyandu terdekat dan jadwal kegiatan." icon={MapPin} - href="#" + href="/user-dashboard/lokasi-posyandu" color="orange" /> @@ -210,7 +210,7 @@ export default async function UserDashboardPage() { title="Artikel Stunting" description="Baca artikel dan informasi edukasi pencegahan stunting." icon={ClipboardList} - href="#" + href="/user-dashboard/artikel" color="purple" /> diff --git a/lint_full.txt b/lint_full.txt new file mode 100644 index 0000000..db06aaa Binary files /dev/null and b/lint_full.txt differ diff --git a/lint_output.txt b/lint_output.txt new file mode 100644 index 0000000..db06aaa Binary files /dev/null and b/lint_output.txt differ diff --git a/tmp/debug_query.ts b/tmp/debug_query.ts new file mode 100644 index 0000000..d934af3 --- /dev/null +++ b/tmp/debug_query.ts @@ -0,0 +1,35 @@ + +import { createClient } from '@supabase/supabase-js' +import dotenv from 'dotenv' +dotenv.config({ path: '.env.local' }) + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + +const supabase = createClient(supabaseUrl, supabaseAnonKey) + +async function debugQuery() { + console.log('Testing query...') + const { data, error } = await supabase + .from('detail_posyandu') + .select(` + *, + petugas:petugas_posyandu_lokal(*), + reviews:posyandu_reviews( + rating, + comment, + created_at, + user:akun_balita(nama_orang_tua) + ) + `) + .limit(1) + + if (error) { + console.error('Full Error Object:', error) + console.error('Error JSON:', JSON.stringify(error, null, 2)) + } else { + console.log('Data fetched successfully:', JSON.stringify(data, null, 2)) + } +} + +debugQuery()