## Perubahan Tampilan Admin

### File Baru (4)
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
- `screens/Admin/Voting/BoxVotingStatus.tsx`

### File Diubah (3)
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
- `app/(application)/admin/voting/history.tsx` → 5 baris
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris

### API Updates (2)
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
- `service/api-admin/api-master-admin.ts` → tambah param `page`

## Fitur Baru
- Pagination (infinite scroll)
- Pull-to-Refresh
- Skeleton Loading
- Empty State
- Search Functionality

### No Issue"
This commit is contained in:
2026-02-18 14:28:15 +08:00
parent 97e1f50660
commit 76845b71b4
25 changed files with 751 additions and 531 deletions

View File

@@ -0,0 +1,54 @@
import { Divider, StackCustom, TextCustom } from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { dateTimeView } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { View } from "react-native";
interface BoxVotingStatusProps {
item: any;
status?: string;
path: any;
}
export default function Admin_BoxVotingStatus({
item,
status,
path,
}: BoxVotingStatusProps) {
return (
<>
<AdminBasicBox
style={{ marginHorizontal: 10, marginVertical: 5 }}
onPress={() => {
router.push(path);
}}
>
<StackCustom gap={0}>
<View style={{ paddingBlock: 8 }}>
<TextCustom size={"large"} bold truncate={2}>
{item?.title || "-"}
</TextCustom>
</View>
<Divider />
<GridSpan_4_8
label={<TextCustom>Mulai</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.awalVote }) || "-"}
</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom>Berakhir</TextCustom>}
value={
<TextCustom>
{dateTimeView({ date: item?.akhirVote }) || "-"}
</TextCustom>
}
/>
</StackCustom>
</AdminBasicBox>
</>
);
}

View File

@@ -0,0 +1,152 @@
import { BadgeCustom, TextCustom } from "@/components";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl, View } from "react-native";
export function Admin_ScreenEventTypeOfEvent() {
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminMasterTypeOfEvent({
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen action plus untuk header
const rightComponent = useMemo(
() => (
<AdminActionIconPlus
onPress={() => {
router.push(`/admin/event/type-create`);
}}
/>
),
[],
);
// Header component untuk title
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle title="Tipe Acara" rightComponent={rightComponent} />
),
[rightComponent],
);
// Render header tabel (Aksi, Status, Tipe Acara)
const renderTableHeader = useMemo(
() => (
<>
<GridViewCustomSpan
span1={2}
span2={5}
span3={5}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={
<TextCustom bold align="center">
Status
</TextCustom>
}
component3={<TextCustom bold>Tipe Acara</TextCustom>}
/>
</>
),
[],
);
// Render item untuk daftar tipe event (mengikuti pattern InformationBankSection)
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
onPress={() => {
router.push(`/admin/event/type-update?id=${item.id}`);
}}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<GridTwoView
leftItem={<TextCustom bold>{item?.name || "-"}</TextCustom>}
rightItem={
<View>
{item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</View>
}
spanLeft={8}
spanRight={4}
styleRight={{
alignItems: "flex-end",
}}
/>
</AdminBasicBox>
),
[],
);
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,102 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
PAGINATION_DEFAULT_TAKE
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxVotingStatus from "./BoxVotingStatus";
export function Admin_ScreenVotingHistory() {
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminVoting({
category: "history",
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxVotingStatus
key={index}
item={item}
path={`/admin/voting/${item.id}/history`}
/>
),
[],
);
// Header component dengan judul voting history
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle title="Riwayat" rightComponent={rightComponent} />
),
[rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -0,0 +1,110 @@
import { SearchInput } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
PAGINATION_DEFAULT_TAKE
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useMemo, useState } from "react";
import { RefreshControl } from "react-native";
import Admin_BoxVotingStatus from "./BoxVotingStatus";
export function Admin_ScreenVotingStatus() {
const { status } = useLocalSearchParams();
const [search, setSearch] = useState<string>("");
// Gunakan hook pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAdminVoting({
category: status as any,
search: searchQuery,
page: String(page),
});
if (response.success) {
return { data: response.data };
} else {
return { data: [] };
}
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [status],
});
// Komponen search input untuk header
const rightComponent = useMemo(
() => (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
),
[search],
);
// Render item untuk daftar voting
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => (
<Admin_BoxVotingStatus
key={index}
item={item}
status={status as string}
path={`/admin/voting/${item.id}/${status}`}
/>
),
[status],
);
// Header component dengan judul status voting
const headerComponent = useMemo(
() => (
<AdminComp_BoxTitle
title={`Voting ${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
),
[status, rightComponent],
);
// Buat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
isInitialLoad: pagination.isInitialLoad,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor="#E1B525"
colors={["#E1B525"]}
/>
}
/>
);
}

View File

@@ -1,12 +1,12 @@
import {
ViewWrapper,
DrawerCustom,
DummyLandscapeImage,
Spacing,
StackCustom,
TextCustom,
Grid,
ButtonCustom,
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
@@ -15,8 +15,8 @@ import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, router } from "expo-router";
import { useState, useCallback } from "react";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import MapView, { Marker } from "react-native-maps";
@@ -47,7 +47,7 @@ export default function MapsView() {
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [])
}, []),
);
const handlerLoadList = async () => {
@@ -146,52 +146,52 @@ export default function MapsView() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.nomorTelepon}</TextCustom>}
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>