diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index da26a972..ddd52c52 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -5,13 +5,17 @@ import { toast } from "react-toastify"; import { proxy } from "valtio"; import { z } from "zod"; -// --- Zod Schema untuk APBDes Item (tanpa field kalkulasi) --- +// --- Zod Schema untuk APBDes Item (dengan field kalkulasi) --- const ApbdesItemSchema = z.object({ kode: z.string().min(1, "Kode wajib diisi"), uraian: z.string().min(1, "Uraian wajib diisi"), anggaran: z.number().min(0, "Anggaran tidak boleh negatif"), level: z.number().int().min(1).max(3), tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(), + // Field kalkulasi dari realisasiItems (auto-calculated di backend) + realisasi: z.number().min(0).default(0), + selisih: z.number().default(0), + persentase: z.number().default(0), }); const ApbdesFormSchema = z.object({ @@ -19,8 +23,9 @@ const ApbdesFormSchema = z.object({ name: z.string().optional(), deskripsi: z.string().optional(), jumlah: z.string().optional(), - imageId: z.string().min(1, "Gambar wajib diunggah"), - fileId: z.string().min(1, "File wajib diunggah"), + // Image dan file opsional (bisa kosong) + imageId: z.string().optional(), + fileId: z.string().optional(), items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"), }); @@ -35,7 +40,7 @@ const defaultApbdesForm = { items: [] as z.infer[], }; -// --- Helper: Normalize item (tanpa kalkulasi, backend yang hitung) --- +// --- Helper: Normalize item (dengan field kalkulasi) --- function normalizeItem(item: Partial>): z.infer { return { kode: item.kode || "", @@ -43,6 +48,9 @@ function normalizeItem(item: Partial>): z.infer anggaran: item.anggaran ?? 0, level: item.level || 1, tipe: item.tipe ?? null, + realisasi: item.realisasi ?? 0, + selisih: item.selisih ?? 0, + persentase: item.persentase ?? 0, }; } @@ -248,6 +256,9 @@ const apbdes = proxy({ kode: item.kode, uraian: item.uraian, anggaran: item.anggaran, + realisasi: item.totalRealisasi || 0, + selisih: item.selisih || 0, + persentase: item.persentase || 0, level: item.level, tipe: item.tipe || 'pendapatan', })), @@ -275,11 +286,24 @@ const apbdes = proxy({ try { this.loading = true; // Include the ID in the request body + // Omit realisasi, selisih, persentase karena itu calculated fields di backend const requestData = { - ...parsed.data, - id: this.id, // Add the ID to the request body + tahun: parsed.data.tahun, + name: parsed.data.name, + deskripsi: parsed.data.deskripsi, + jumlah: parsed.data.jumlah, + imageId: parsed.data.imageId, + fileId: parsed.data.fileId, + id: this.id, + items: parsed.data.items.map(item => ({ + kode: item.kode, + uraian: item.uraian, + anggaran: item.anggaran, + level: item.level, + tipe: item.tipe ?? null, + })), }; - + const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData); if (res.data?.success) { diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx index 31fb2faf..59d7006c 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx @@ -44,6 +44,9 @@ type ItemForm = { anggaran: number; level: number; tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; + realisasi?: number; + selisih?: number; + persentase?: number; }; function EditAPBDes() { @@ -72,6 +75,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); // Simpan data original untuk reset form @@ -125,9 +131,9 @@ function EditAPBDes() { kode: item.kode, uraian: item.uraian, anggaran: item.anggaran, - realisasi: item.realisasi, - selisih: item.selisih, - persentase: item.persentase, + realisasi: item.totalRealisasi || 0, + selisih: item.selisih || 0, + persentase: item.persentase || 0, level: item.level, tipe: item.tipe || 'pendapatan', })), @@ -155,7 +161,7 @@ function EditAPBDes() { }; const handleAddItem = () => { - const { kode, uraian, anggaran, level, tipe } = newItem; + const { kode, uraian, anggaran, level, tipe, realisasi, selisih, persentase } = newItem; if (!kode || !uraian) { return toast.warn('Kode dan uraian wajib diisi'); } @@ -166,6 +172,9 @@ function EditAPBDes() { kode, uraian, anggaran, + realisasi: realisasi || 0, + selisih: selisih || 0, + persentase: persentase || 0, level, tipe: finalTipe, }); @@ -176,6 +185,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); }; @@ -193,7 +205,6 @@ function EditAPBDes() { // Upload file baru jika ada perubahan if (imageFile) { - // Hapus file lama dari form jika ada file baru const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name, @@ -205,7 +216,6 @@ function EditAPBDes() { } if (docFile) { - // Hapus file lama dari form jika ada file baru const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name, @@ -216,15 +226,7 @@ function EditAPBDes() { } } - // Jika tidak ada file baru, gunakan ID lama (sudah ada di form) - // Pastikan imageId dan fileId tetap ada - if (!apbdesState.edit.form.imageId) { - return toast.warn('Gambar wajib diunggah'); - } - if (!apbdesState.edit.form.fileId) { - return toast.warn('Dokumen wajib diunggah'); - } - + // Image dan file sekarang opsional, tidak perlu validasi const success = await apbdesState.edit.update(); if (success) { router.push('/admin/landing-page/apbdes'); @@ -264,6 +266,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); toast.info('Form dikembalikan ke data awal'); @@ -328,11 +333,11 @@ function EditAPBDes() { required /> - {/* Gambar & Dokumen */} + {/* Gambar & Dokumen (Opsional) */} - Gambar APBDes + Gambar APBDes (Opsional) { setPreviewImage(null); setImageFile(null); + apbdesState.edit.form.imageId = ''; // Clear imageId from form }} > @@ -382,7 +388,7 @@ function EditAPBDes() { - Dokumen APBDes + Dokumen APBDes (Opsional) { setPreviewDoc(null); setDocFile(null); + apbdesState.edit.form.fileId = ''; // Clear fileId from form }} > @@ -527,6 +534,9 @@ function EditAPBDes() { Kode Uraian Anggaran + Realisasi + Selisih + % Level Tipe Aksi @@ -542,6 +552,11 @@ function EditAPBDes() { {item.uraian} {item.anggaran.toLocaleString('id-ID')} + {item.realisasi?.toLocaleString('id-ID') || '0'} + 0 ? 'red' : 'green' }}> + {item.selisih?.toLocaleString('id-ID') || '0'} + + {item.persentase?.toFixed(2) || '0'}% L{item.level} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx index e4990717..81e8fff3 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx @@ -46,13 +46,9 @@ function CreateAPBDes() { const [docFile, setDocFile] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - // Check if form is valid + // Check if form is valid - hanya cek items, gambar dan file opsional const isFormValid = () => { - return ( - imageFile !== null && - docFile !== null && - stateAPBDes.create.form.items.length > 0 - ); + return stateAPBDes.create.form.items.length > 0; }; // Form sementara untuk input item baru @@ -84,28 +80,34 @@ function CreateAPBDes() { }; const handleSubmit = async () => { - if (!imageFile || !docFile) { - return toast.warn("Pilih gambar dan dokumen terlebih dahulu"); - } if (stateAPBDes.create.form.items.length === 0) { return toast.warn("Minimal tambahkan 1 item APBDes"); } try { setIsSubmitting(true); - const [uploadImageRes, uploadDocRes] = await Promise.all([ - ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }), - ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }), - ]); - const imageId = uploadImageRes?.data?.data?.id; - const fileId = uploadDocRes?.data?.data?.id; + // Upload files hanya jika ada file yang dipilih + let imageId = ''; + let fileId = ''; - if (!imageId || !fileId) { - return toast.error("Gagal mengupload file"); + if (imageFile) { + const uploadImageRes = await ApiFetch.api.fileStorage.create.post({ + file: imageFile, + name: imageFile.name, + }); + imageId = uploadImageRes?.data?.data?.id || ''; } - // Update form dengan ID file + if (docFile) { + const uploadDocRes = await ApiFetch.api.fileStorage.create.post({ + file: docFile, + name: docFile.name, + }); + fileId = uploadDocRes?.data?.data?.id || ''; + } + + // Update form dengan ID file (bisa kosong) stateAPBDes.create.form.imageId = imageId; stateAPBDes.create.form.fileId = fileId; @@ -174,12 +176,16 @@ function CreateAPBDes() { style={{ border: '1px solid #e0e0e0' }} > - {/* Gambar & Dokumen (dipendekkan untuk fokus pada items) */} + {/* Info: File opsional */} + + * Upload gambar dan dokumen bersifat opsional. Bisa dikosongkan jika belum ada. + + {/* Gambar APBDes */} - Gambar APBDes + Gambar APBDes (Opsional) { @@ -249,10 +255,10 @@ function CreateAPBDes() { )} - {/* Dokumen APBDes */} + {/* Dokumen APBDes (Opsional) */} - Dokumen APBDes + Dokumen APBDes (Opsional) { diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts index 9a7235de..7ce23625 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts @@ -17,8 +17,8 @@ type FormCreate = { name?: string; deskripsi?: string; jumlah?: string; - imageId: string; - fileId: string; + imageId?: string | null; // Opsional + fileId?: string | null; // Opsional items: APBDesItemInput[]; }; @@ -32,12 +32,7 @@ export default async function apbdesCreate(context: Context) { if (!body.tahun) { throw new Error('Tahun is required'); } - if (!body.imageId) { - throw new Error('Image ID is required'); - } - if (!body.fileId) { - throw new Error('File ID is required'); - } + // Image dan file sekarang opsional if (!body.items || body.items.length === 0) { throw new Error('At least one item is required'); } @@ -50,8 +45,8 @@ export default async function apbdesCreate(context: Context) { name: body.name || `APBDes Tahun ${body.tahun}`, deskripsi: body.deskripsi, jumlah: body.jumlah, - imageId: body.imageId, - fileId: body.fileId, + imageId: body.imageId || null, // null jika tidak ada + fileId: body.fileId || null, // null jika tidak ada }, }); diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts index c3f6aa61..a3505d64 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts @@ -36,8 +36,8 @@ const APBDes = new Elysia({ name: t.Optional(t.String()), deskripsi: t.Optional(t.String()), jumlah: t.Optional(t.String()), - imageId: t.String(), - fileId: t.String(), + imageId: t.Optional(t.String()), + fileId: t.Optional(t.String()), items: t.Array(ApbdesItemSchema), }), }) @@ -50,8 +50,8 @@ const APBDes = new Elysia({ name: t.Optional(t.String()), deskripsi: t.Optional(t.String()), jumlah: t.Optional(t.String()), - imageId: t.String(), - fileId: t.String(), + imageId: t.Optional(t.String()), + fileId: t.Optional(t.String()), items: t.Array(ApbdesItemSchema), }), }) diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts index bd4b4c3e..424ece54 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts @@ -1,6 +1,7 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; import { assignParentIdsToApbdesItems } from "./lib/getParentsID"; +import { RealisasiItem } from "@prisma/client"; type APBDesItemInput = { kode: string; @@ -15,8 +16,8 @@ type FormUpdateBody = { name?: string; deskripsi?: string; jumlah?: string; - imageId: string; - fileId: string; + imageId?: string | null; + fileId?: string | null; items: APBDesItemInput[]; }; @@ -28,6 +29,16 @@ export default async function apbdesUpdate(context: Context) { // 1. Pastikan APBDes ada const existing = await prisma.aPBDes.findUnique({ where: { id }, + include: { + items: { + where: { isActive: true }, + include: { + realisasiItems: { + where: { isActive: true }, + }, + }, + }, + }, }); if (!existing) { @@ -38,17 +49,35 @@ export default async function apbdesUpdate(context: Context) { }; } - // 2. Hapus semua item lama (cascade akan menghapus realisasiItems juga) + // 2. Build map untuk preserve realisasiItems berdasarkan kode + const existingItemsMap = new Map(); + + existing.items.forEach(item => { + existingItemsMap.set(item.kode, { + id: item.id, + realisasiItems: item.realisasiItems, + }); + }); + + // 3. Hapus semua item lama (cascade akan menghapus realisasiItems juga) + // TAPI kita sudah save realisasiItems di map atas await prisma.aPBDesItem.deleteMany({ where: { apbdesId: id }, }); - // 3. Buat item baru dengan auto-calculate fields + // 4. Buat item baru dengan preserve realisasiItems await prisma.aPBDesItem.createMany({ - data: body.items.map((item) => { + data: await Promise.all(body.items.map(async (item) => { const anggaran = item.anggaran; - const totalRealisasi = 0; // Reset karena items baru - const selisih = anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan) + + // Check apakah item ini punya realisasiItems lama + const existingItem = existingItemsMap.get(item.kode); + const realisasiItemsData = existingItem?.realisasiItems || []; + const totalRealisasi = realisasiItemsData.reduce((sum, r) => sum + r.jumlah, 0); + const selisih = anggaran - totalRealisasi; const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0; return { @@ -63,16 +92,68 @@ export default async function apbdesUpdate(context: Context) { persentase, isActive: true, }; - }), + })), }); - // 4. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya + // 5. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya const allItems = await prisma.aPBDesItem.findMany({ where: { apbdesId: id }, select: { id: true, kode: true }, }); - // 5. Update parentId untuk setiap item + // 6. Build map baru untuk item IDs + const newItemIdsMap = new Map(); + allItems.forEach(item => { + newItemIdsMap.set(item.kode, item.id); + }); + + // 7. Re-create realisasiItems dengan link ke item IDs yang baru + for (const [oldKode, oldItemData] of existingItemsMap.entries()) { + if (oldItemData.realisasiItems.length > 0) { + const newItemId = newItemIdsMap.get(oldKode); + if (newItemId) { + // Re-create realisasiItems untuk item ini + await prisma.realisasiItem.createMany({ + data: oldItemData.realisasiItems.map(r => ({ + apbdesItemId: newItemId, + kode: r.kode, + jumlah: r.jumlah, + tanggal: r.tanggal, + keterangan: r.keterangan, + buktiFileId: r.buktiFileId, + isActive: true, + })), + }); + } + } + } + + // 8. Recalculate totalRealisasi setelah re-create realisasiItems + for (const kode of existingItemsMap.keys()) { + const newItemId = newItemIdsMap.get(kode); + if (newItemId) { + const realisasiItems = await prisma.realisasiItem.findMany({ + where: { apbdesItemId: newItemId, isActive: true }, + }); + const totalRealisasi = realisasiItems.reduce((sum, r) => sum + r.jumlah, 0); + + const item = await prisma.aPBDesItem.findUnique({ + where: { id: newItemId }, + }); + + if (item) { + const selisih = item.anggaran - totalRealisasi; + const persentase = item.anggaran > 0 ? (totalRealisasi / item.anggaran) * 100 : 0; + + await prisma.aPBDesItem.update({ + where: { id: newItemId }, + data: { totalRealisasi, selisih, persentase }, + }); + } + } + } + + // 9. Update parentId untuk setiap item const itemsForParentUpdate = allItems.map(item => ({ id: item.id, kode: item.kode, @@ -80,7 +161,7 @@ export default async function apbdesUpdate(context: Context) { await assignParentIdsToApbdesItems(itemsForParentUpdate); - // 6. Update data APBDes + // 10. Update data APBDes await prisma.aPBDes.update({ where: { id }, data: { @@ -88,12 +169,12 @@ export default async function apbdesUpdate(context: Context) { name: body.name || `APBDes Tahun ${body.tahun}`, deskripsi: body.deskripsi, jumlah: body.jumlah, - imageId: body.imageId, - fileId: body.fileId, + imageId: body.imageId === '' ? null : body.imageId, + fileId: body.fileId === '' ? null : body.fileId, }, }); - // 7. Ambil data lengkap untuk response (include realisasiItems) + // 11. Ambil data lengkap untuk response (include realisasiItems) const result = await prisma.aPBDes.findUnique({ where: { id }, include: { diff --git a/src/app/context/MusicContext.tsx b/src/app/context/MusicContext.tsx index 60119836..19020151 100644 --- a/src/app/context/MusicContext.tsx +++ b/src/app/context/MusicContext.tsx @@ -82,6 +82,12 @@ export function MusicProvider({ children }: { children: ReactNode }) { const audioRef = useRef(null); const isSeekingRef = useRef(false); const animationFrameRef = useRef(null); + const isRepeatRef = useRef(false); // Ref untuk avoid stale closure + + // Sync ref dengan state + useEffect(() => { + isRepeatRef.current = isRepeat; + }, [isRepeat]); // Load musik data const loadMusikData = useCallback(async () => { @@ -111,7 +117,8 @@ export function MusicProvider({ children }: { children: ReactNode }) { }); audioRef.current.addEventListener('ended', () => { - if (isRepeat) { + // Gunakan ref untuk avoid stale closure + if (isRepeatRef.current) { audioRef.current!.currentTime = 0; audioRef.current!.play(); } else { @@ -132,7 +139,7 @@ export function MusicProvider({ children }: { children: ReactNode }) { } }; // eslint-disable-next-line react-hooks/exhaustive-deps -- playNext is intentionally not in deps to avoid circular dependency - }, [loadMusikData, isRepeat]); + }, [loadMusikData]); // Remove isRepeat dari deps karena sudah pakai ref // Update time with requestAnimationFrame for smooth progress const updateTime = useCallback(() => { diff --git a/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress.tsx b/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress.tsx index a1d3df88..cf93ab32 100644 --- a/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress.tsx +++ b/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress.tsx @@ -78,7 +78,8 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) { // Hitung total per kategori const calcTotal = (items: { anggaran: number; realisasi: number }[]) => { const anggaran = items.reduce((sum, item) => sum + item.anggaran, 0); - const realisasi = items.reduce((sum, item) => sum + item.realisasi, 0); + // Use realisasi field (already mapped from totalRealisasi in transformAPBDesData) + const realisasi = items.reduce((sum, item) => sum + (item.realisasi || 0), 0); const persen = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; return { anggaran, realisasi, persen }; }; diff --git a/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaTable.tsx b/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaTable.tsx index 9b0791f8..c4e4de6a 100644 --- a/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaTable.tsx +++ b/src/app/darmasaba/(tambahan)/apbdes/lib/apbDesaTable.tsx @@ -68,6 +68,7 @@ function APBDesTable({ apbdesData }: APBDesTableProps) { // Calculate totals const totalAnggaran = items.reduce((sum, item) => sum + (item.anggaran || 0), 0); + // Use realisasi field (already mapped from totalRealisasi in transformAPBDesData) const totalRealisasi = items.reduce((sum, item) => sum + (item.realisasi || 0), 0); const totalSelisih = totalAnggaran - totalRealisasi; const totalPersentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0; diff --git a/src/app/darmasaba/(tambahan)/apbdes/lib/types.ts b/src/app/darmasaba/(tambahan)/apbdes/lib/types.ts index 3c6dde79..fcef7b0a 100644 --- a/src/app/darmasaba/(tambahan)/apbdes/lib/types.ts +++ b/src/app/darmasaba/(tambahan)/apbdes/lib/types.ts @@ -51,7 +51,8 @@ export function transformAPBDesData(data: any): APBDesData { kode: item.kode || '', uraian: item.uraian || '', anggaran: typeof item.anggaran === 'number' ? item.anggaran : 0, - realisasi: typeof item.realisasi === 'number' ? item.realisasi : 0, + // Map totalRealisasi from backend to realisasi field + realisasi: typeof item.totalRealisasi === 'number' ? item.totalRealisasi : (typeof item.realisasi === 'number' ? item.realisasi : 0), selisih: typeof item.selisih === 'number' ? item.selisih : 0, persentase: typeof item.persentase === 'number' ? item.persentase : 0, level: typeof item.level === 'number' ? item.level : 1, diff --git a/src/app/darmasaba/_com/FixedPlayerBar.tsx b/src/app/darmasaba/_com/FixedPlayerBar.tsx index 306cd89f..f05a31cc 100644 --- a/src/app/darmasaba/_com/FixedPlayerBar.tsx +++ b/src/app/darmasaba/_com/FixedPlayerBar.tsx @@ -3,6 +3,7 @@ import { ActionIcon, Avatar, Box, + Button, Flex, Group, Paper, @@ -12,6 +13,7 @@ import { } from '@mantine/core'; import { IconArrowsShuffle, + IconMusic, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, @@ -45,7 +47,7 @@ export default function FixedPlayerBar() { } = useMusic(); const [showVolume, setShowVolume] = useState(false); - const [isPlayerVisible, setIsPlayerVisible] = useState(true); + const [isMinimized, setIsMinimized] = useState(false); // Format time const formatTime = (seconds: number) => { @@ -69,12 +71,55 @@ export default function FixedPlayerBar() { toggleShuffle(); }; - // Handle close player - const handleClosePlayer = () => { - setIsPlayerVisible(false); + // Handle minimize player (show floating icon) + const handleMinimizePlayer = () => { + setIsMinimized(true); }; - if (!currentSong || !isPlayerVisible) { + // Handle restore player from floating icon + const handleRestorePlayer = () => { + setIsMinimized(false); + }; + + // If minimized, show floating icon instead of player bar + if (isMinimized) { + return ( + <> + {/* Floating Music Icon - Shows when player is minimized */} + + + {/* Spacer to prevent content from being hidden behind player */} + + + ); + } + + if (!currentSong) { return null; } @@ -89,12 +134,12 @@ export default function FixedPlayerBar() { p="sm" shadow="lg" style={{ - zIndex: 1000, + zIndex: 1, borderTop: '1px solid rgba(0,0,0,0.1)', }} > - {/* Song Info */} + {/* Song Info - Left */} - {/* Controls */} - - - - + {/* Controls + Progress - Center */} + + {/* Control Buttons */} + + + + - - - + + + - - {isPlaying ? ( - - ) : ( - - )} - + + {isPlaying ? ( + + ) : ( + + )} + - - - + + + - - {isRepeat ? : } - + + {isRepeat ? : } + + + + {/* Progress Bar - Desktop */} + + formatTime(value)} + /> + - {/* Progress Bar - Desktop */} - - formatTime(value)} - /> - - - {/* Right Controls */} - + {/* Right Controls - Volume + Close */} + setShowVolume(true)} onMouseLeave={() => setShowVolume(false)} @@ -241,8 +289,8 @@ export default function FixedPlayerBar() { variant="subtle" color="gray" size="lg" - onClick={handleClosePlayer} - title="Close player" + onClick={handleMinimizePlayer} + title="Minimize player" > diff --git a/src/app/darmasaba/_com/NewsReaderalanding.tsx b/src/app/darmasaba/_com/NewsReaderalanding.tsx index a4a1755f..d56125f4 100644 --- a/src/app/darmasaba/_com/NewsReaderalanding.tsx +++ b/src/app/darmasaba/_com/NewsReaderalanding.tsx @@ -1,6 +1,6 @@ 'use client'; import { Button } from '@mantine/core'; -import { IconMusic, IconMusicOff } from '@tabler/icons-react'; +import { IconDisabled, IconDisabledOff } from '@tabler/icons-react'; import { useEffect, useRef, useState } from 'react'; const NewsReaderLanding = () => { @@ -95,15 +95,17 @@ const NewsReaderLanding = () => { mt="md" style={{ position: 'fixed', - bottom: '350px', + top: '50%', // Menempatkan titik atas ikon di tengah layar left: '0px', + transform: 'translateY(80%)', // Menggeser ikon ke atas sebesar setengah tingginya sendiri agar benar-benar di tengah borderBottomRightRadius: '20px', borderTopRightRadius: '20px', - transition: 'all 0.3s ease', + cursor: 'pointer', + transition: 'transform 0.2s', zIndex: 1 }} > - {isPointerMode ? : } + {isPointerMode ? : } ); }; diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx index 4307ebef..26d5798a 100644 --- a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx +++ b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx @@ -5,7 +5,8 @@ function Summary({ title, data }: any) { if (!data || data.length === 0) return null; const totalAnggaran = data.reduce((s: number, i: any) => s + i.anggaran, 0); - const totalRealisasi = data.reduce((s: number, i: any) => s + i.totalRealisasi, 0); + // Use realisasi field (already mapped from totalRealisasi in transformAPBDesData) + const totalRealisasi = data.reduce((s: number, i: any) => s + (i.realisasi || i.totalRealisasi || 0), 0); const persen = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0; @@ -87,7 +88,8 @@ export default function GrafikRealisasi({ apbdesData }: any) { // Hitung total keseluruhan const totalAnggaranSemua = items.reduce((s: number, i: any) => s + i.anggaran, 0); - const totalRealisasiSemua = items.reduce((s: number, i: any) => s + i.totalRealisasi, 0); + // Use realisasi field (already mapped from totalRealisasi in transformAPBDesData) + const totalRealisasiSemua = items.reduce((s: number, i: any) => s + (i.realisasi || i.totalRealisasi || 0), 0); const persenSemua = totalAnggaranSemua > 0 ? (totalRealisasiSemua / totalAnggaranSemua) * 100 : 0; const formatRupiah = (angka: number) => { @@ -105,8 +107,14 @@ export default function GrafikRealisasi({ apbdesData }: any) { GRAFIK REALISASI APBDes {tahun} - {/* Summary Total Keseluruhan */} - + + + + + + + {/* Summary Total Keseluruhan + <> TOTAL KESELURUHAN @@ -124,13 +132,7 @@ export default function GrafikRealisasi({ apbdesData }: any) { color={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'} /> - - - - - - - + */} ); } \ No newline at end of file