feat: tambah fitur kalender umum village dengan indikator per jenis event
- Tambah halaman /village-calendar dengan monthly grid dan agenda view - Tampilkan acara divisi (DivisionCalendarReminder) dan kegiatan (ProjectTask) se-village - Indikator dot dua warna pada kalender: ungu untuk acara divisi, biru-abu untuk kegiatan - Tambah endpoint apiGetVillageCalendarByDate dan apiGetVillageCalendarIndicator - Tambah menu Kalender di halaman /feature dengan grid layout flexWrap - Sesuaikan warna EventItem dengan TYPE_COLORS village-calendar - Pindahkan inline style ke Styles.ts sebagai class baru
This commit is contained in:
@@ -23,13 +23,12 @@ export default function Feature() {
|
||||
}}
|
||||
/>
|
||||
<View style={[Styles.p15]}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mb15]}>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', rowGap: 20 }}>
|
||||
<ButtonFiturMenu icon={<Feather name="users" size={30} color={colors.icon} />} text="Divisi" onPress={() => { router.push('/division?active=true') }} />
|
||||
<ButtonFiturMenu icon={<Feather name="bar-chart" size={30} color={colors.icon} />} text="Kegiatan" onPress={() => { router.push('/project?status=0') }} />
|
||||
<ButtonFiturMenu icon={<Ionicons name="megaphone-outline" size={30} color={colors.icon} />} text="Pengumuman" onPress={() => { router.push('/announcement') }} />
|
||||
<ButtonFiturMenu icon={<Ionicons name="chatbubbles-outline" size={30} color={colors.icon} />} text="Diskusi" onPress={() => { router.push('/discussion?active=true') }} />
|
||||
</View>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mb15, (entityUser.role == 'cosupadmin' ? Styles.w70 : entityUser.role == 'supadmin' || entityUser.role == 'developer' ? Styles.w100 : Styles.w40)]}>
|
||||
<ButtonFiturMenu icon={<Feather name="calendar" size={30} color={colors.icon} />} text="Kalender" onPress={() => { router.push('/village-calendar') }} />
|
||||
<ButtonFiturMenu icon={<MaterialCommunityIcons name="account-group-outline" size={30} color={colors.icon} />} text="Anggota" onPress={() => { router.push('/member') }} />
|
||||
<ButtonFiturMenu icon={<MaterialCommunityIcons name="account-tie-outline" size={30} color={colors.icon} />} text="Jabatan" onPress={() => { router.push('/position') }} />
|
||||
{
|
||||
@@ -39,7 +38,6 @@ export default function Feature() {
|
||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||
<>
|
||||
<ButtonFiturMenu icon={<Ionicons name="bookmarks-outline" size={30} color={colors.icon} />} text="Lembaga Desa" onPress={() => { router.push('/group') }} />
|
||||
{/* <ButtonFiturMenu icon={<Ionicons name="color-palette-sharp" size={30} color={colors.icon} />} text="Tema" onPress={() => { }} /> */}
|
||||
<ButtonFiturMenu icon={<Ionicons name="images-outline" size={30} color={colors.icon} />} text="Banner" onPress={() => { router.push('/banner') }} />
|
||||
</>
|
||||
}
|
||||
|
||||
294
app/(application)/village-calendar/index.tsx
Normal file
294
app/(application)/village-calendar/index.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import ItemDateCalendar from "@/components/calendar/itemDateCalendar";
|
||||
import Skeleton from "@/components/skeleton";
|
||||
import Text from "@/components/Text";
|
||||
import Styles from "@/constants/Styles";
|
||||
import { apiGetVillageCalendarByDate, apiGetVillageCalendarIndicator } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { router, Stack } from "expo-router";
|
||||
import 'intl';
|
||||
import 'intl/locale-data/jsonp/id';
|
||||
import moment from "moment";
|
||||
import "moment/locale/id";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import Datepicker, {
|
||||
CalendarComponents,
|
||||
CalendarDay
|
||||
} from "react-native-ui-datepicker";
|
||||
moment.locale('id');
|
||||
|
||||
type EventItem = {
|
||||
id: string;
|
||||
type: 'calendar' | 'task';
|
||||
title: string;
|
||||
desc: string;
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
timeStart: string | null;
|
||||
timeEnd: string | null;
|
||||
divisionName: string | null;
|
||||
projectName: string | null;
|
||||
idDivision: string | null;
|
||||
idRef: string;
|
||||
};
|
||||
|
||||
const TYPE_COLORS = {
|
||||
calendar: {
|
||||
light: { bg: '#A9B5DF', stick: '#7886C7' },
|
||||
dark: { bg: '#2D2B5E', stick: '#7886C7' },
|
||||
},
|
||||
task: {
|
||||
light: { bg: '#D6E6F2', stick: '#94B4C1' },
|
||||
dark: { bg: '#1C3347', stick: '#94B4C1' },
|
||||
},
|
||||
};
|
||||
|
||||
function VillageEventItem({ item, onPress }: { item: EventItem; onPress: () => void }) {
|
||||
const { activeTheme, colors } = useTheme();
|
||||
const palette = TYPE_COLORS[item.type][activeTheme === 'dark' ? 'dark' : 'light'];
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[Styles.itemEvent, { backgroundColor: palette.bg }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={[Styles.dividerEvent, { backgroundColor: palette.stick }]} />
|
||||
<View style={[Styles.flex1]}>
|
||||
{item.timeStart ? (
|
||||
<Text style={[Styles.textMediumNormal, { color: activeTheme === 'dark' ? '#ffffff' : '#000000' }]}>
|
||||
{item.timeStart} - {item.timeEnd}
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={[Styles.textMediumNormal, { color: activeTheme === 'dark' ? '#ffffff' : '#000000' }]}>
|
||||
{moment(item.dateStart).format('D MMM YYYY')} – {moment(item.dateEnd).format('D MMM YYYY')}
|
||||
</Text>
|
||||
)}
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textDefaultSemiBold, { color: colors.text }]}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<View style={[Styles.villageEventBadge, { backgroundColor: palette.stick }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: '#ffffff' }]}>
|
||||
{item.type === 'calendar' ? 'Divisi' : 'Kegiatan'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textMediumNormal, { color: activeTheme === 'dark' ? '#ffffff' : '#000000', flex: 1 }]}>
|
||||
{item.type === 'calendar' ? item.divisionName : item.projectName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
export default function VillageCalendar() {
|
||||
const { colors, activeTheme } = useTheme();
|
||||
const { token, decryptToken } = useAuthSession();
|
||||
const [selected, setSelected] = useState<any>(new Date());
|
||||
const [data, setData] = useState<EventItem[]>([]);
|
||||
const [dataIndicator, setDataIndicator] = useState<{ calendar: string[], task: string[] }>({ calendar: [], task: [] });
|
||||
const [month, setMonth] = useState<number>(new Date().getMonth());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingBtn, setLoadingBtn] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
async function handleLoad(showLoading: boolean) {
|
||||
try {
|
||||
setLoading(showLoading);
|
||||
const hasil = await decryptToken(String(token?.current));
|
||||
const response = await apiGetVillageCalendarByDate({
|
||||
user: hasil,
|
||||
date: moment(selected).format("YYYY-MM-DD"),
|
||||
});
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadIndicator() {
|
||||
try {
|
||||
setLoadingBtn(true);
|
||||
const newDate = new Date(selected?.getFullYear(), month, 1);
|
||||
const hasil = await decryptToken(String(token?.current));
|
||||
const response = await apiGetVillageCalendarIndicator({
|
||||
user: hasil,
|
||||
date: moment(newDate).format("YYYY-MM-DD"),
|
||||
});
|
||||
setDataIndicator(response.data ?? { calendar: [], task: [] });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setTimeout(() => setLoadingBtn(false), 500);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(true);
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
handleLoadIndicator();
|
||||
}, [month]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
handleLoad(false);
|
||||
handleLoadIndicator();
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
function handlePressEvent(item: EventItem) {
|
||||
if (item.type === 'calendar') {
|
||||
router.push(`/division/${item.idDivision}/calendar/${item.id}` as any);
|
||||
} else {
|
||||
router.push(`/project/${item.idRef}` as any);
|
||||
}
|
||||
}
|
||||
|
||||
const calendarEvents = data.filter(e => e.type === 'calendar');
|
||||
const taskEvents = data.filter(e => e.type === 'task');
|
||||
|
||||
const components: CalendarComponents = {
|
||||
Day: (day: CalendarDay) => {
|
||||
const today = moment(String(day.date)).format("YYYY-MM-DD");
|
||||
return (
|
||||
<ItemDateCalendar
|
||||
text={day.text}
|
||||
isSelected={day.isSelected}
|
||||
isSignCalendar={dataIndicator.calendar.includes(today)}
|
||||
isSignTask={dataIndicator.task.includes(today)}
|
||||
onPress={() => setSelected(new Date(today))}
|
||||
/>
|
||||
);
|
||||
},
|
||||
IconNext: (
|
||||
<Pressable onPress={() => !loadingBtn && setMonth(month + 1)}>
|
||||
<Feather name="chevron-right" size={20} color={loadingBtn ? 'gray' : colors.text} />
|
||||
</Pressable>
|
||||
),
|
||||
IconPrev: (
|
||||
<Pressable onPress={() => !loadingBtn && setMonth(month - 1)}>
|
||||
<Feather name="chevron-left" size={20} color={loadingBtn ? 'gray' : colors.text} />
|
||||
</Pressable>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerShown: true,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Kalender"
|
||||
showBack={true}
|
||||
onPressLeft={() => router.back()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
style={[Styles.h100]}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
{/* Calendar grid */}
|
||||
<View style={[Styles.wrapPaper, Styles.p10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
<Datepicker
|
||||
components={components}
|
||||
mode="single"
|
||||
date={selected}
|
||||
month={month}
|
||||
onMonthChange={(m) => setMonth(m)}
|
||||
styles={{
|
||||
selected: Styles.selectedDate,
|
||||
month_label: { color: colors.text },
|
||||
month_selector_label: { color: colors.text },
|
||||
year_label: { color: colors.text },
|
||||
year_selector_label: { color: colors.text },
|
||||
day_label: { color: colors.text },
|
||||
time_label: { color: colors.text },
|
||||
weekday_label: { color: colors.text },
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Legend */}
|
||||
<View style={[Styles.rowItemsCenter, Styles.villageEventLegendRow]}>
|
||||
<View style={[Styles.rowItemsCenter, Styles.villageEventLegendItem]}>
|
||||
<View style={[Styles.villageEventLegendDot, { backgroundColor: '#7886C7' }]} />
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Acara Divisi</Text>
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.villageEventLegendItem]}>
|
||||
<View style={[Styles.villageEventLegendDot, { backgroundColor: '#94B4C1' }]} />
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Kegiatan</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Calendar events */}
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Acara Divisi</Text>
|
||||
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
|
||||
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
|
||||
</>
|
||||
) : calendarEvents.length > 0 ? (
|
||||
calendarEvents.map((item, index) => (
|
||||
<VillageEventItem
|
||||
key={index}
|
||||
item={item}
|
||||
onPress={() => handlePressEvent(item)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>
|
||||
Tidak ada acara
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Task events */}
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Kegiatan</Text>
|
||||
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
|
||||
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
|
||||
</>
|
||||
) : taskEvents.length > 0 ? (
|
||||
taskEvents.map((item, index) => (
|
||||
<VillageEventItem
|
||||
key={index}
|
||||
item={item}
|
||||
onPress={() => handlePressEvent(item)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>
|
||||
Tidak ada kegiatan
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user