fix-admin-menu-desa-potensi

This commit is contained in:
2026-02-25 15:41:01 +08:00
parent b1d28a8322
commit 22de1aa1f3
5 changed files with 89 additions and 22 deletions

View File

@@ -633,25 +633,25 @@ model KategoriBerita {
// ========================================= POTENSI DESA ========================================= // // ========================================= POTENSI DESA ========================================= //
model PotensiDesa { model PotensiDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String @unique @db.VarChar(255)
deskripsi String deskripsi String @db.Text
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id]) kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
kategoriId String? kategoriId String @db.VarChar(36)
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
content String @db.Text content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
} }
model KategoriPotensi { model KategoriPotensi {
id String @id @default(cuid()) id String @id @default(cuid())
nama String nama String @unique @db.VarChar(100)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime?
isActive Boolean @default(true) isActive Boolean @default(true)
PotensiDesa PotensiDesa[] PotensiDesa PotensiDesa[]
} }

View File

@@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import DOMPurify from 'dompurify';
export default function DetailPotensi() { export default function DetailPotensi() {
const router = useRouter(); const router = useRouter();
@@ -77,7 +78,17 @@ export default function DetailPotensi() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}></Text> <Text
fz="md"
c="dimmed"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(data.deskripsi || '-', {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
></Text>
</Box> </Box>
<Box> <Box>
@@ -102,7 +113,12 @@ export default function DetailPotensi() {
<Text <Text
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: data.content || '-' }} dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(data.content || '-', {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>

View File

@@ -27,6 +27,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import DOMPurify from 'dompurify';
function Potensi() { function Potensi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -137,7 +138,12 @@ function ListPotensi({ search }: { search: string }) {
fz="sm" fz="sm"
lh={1.5} lh={1.5}
lineClamp={2} lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
style={{ wordBreak: 'break-word' }} style={{ wordBreak: 'break-word' }}
/> />
</TableTd> </TableTd>
@@ -199,7 +205,12 @@ function ListPotensi({ search }: { search: string }) {
<Text <Text
fz="sm" fz="sm"
lh={1.5} lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
style={{ wordBreak: 'break-word' }} style={{ wordBreak: 'break-word' }}
/> />
</Box> </Box>

View File

@@ -21,8 +21,13 @@ export default async function findUnique(
}, { status: 400 }); }, { status: 400 });
} }
const data = await prisma.potensiDesa.findUnique({ // ✅ Filter by isActive and deletedAt
where: { id }, const data = await prisma.potensiDesa.findFirst({
where: {
id,
isActive: true,
deletedAt: null,
},
include: { include: {
image: true, image: true,
kategori: true kategori: true
@@ -48,5 +53,5 @@ export default async function findUnique(
message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'), message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 }); }, { status: 500 });
} }
} }

View File

@@ -2,15 +2,50 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
export default async function kategoriPotensiDelete(context: Context) { export default async function kategoriPotensiDelete(context: Context) {
const id = context.params.id as string; try {
const id = context.params?.id as string;
await prisma.kategoriPotensi.delete({ if (!id) {
where: { id }, return Response.json({
}); success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
return { // ✅ Cek apakah kategori masih digunakan oleh potensi desa
status: 200, const existingPotensi = await prisma.potensiDesa.findFirst({
success: true, where: {
message: "Sukses Menghapus kategori potensi", kategoriId: id,
}; isActive: true,
deletedAt: null,
},
});
if (existingPotensi) {
return Response.json({
success: false,
message: "Kategori masih digunakan oleh potensi desa. Tidak dapat dihapus.",
}, { status: 400 });
}
// Soft delete
await prisma.kategoriPotensi.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false,
},
});
return {
success: true,
message: "Kategori potensi berhasil dihapus",
};
} catch (error) {
console.error("Delete kategori error:", error);
return Response.json({
success: false,
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
} }