diff --git a/app/(application)/feature.tsx b/app/(application)/feature.tsx index 00b2311..16c9498 100644 --- a/app/(application)/feature.tsx +++ b/app/(application)/feature.tsx @@ -23,13 +23,12 @@ export default function Feature() { }} /> - + } text="Divisi" onPress={() => { router.push('/division?active=true') }} /> } text="Kegiatan" onPress={() => { router.push('/project?status=0') }} /> } text="Pengumuman" onPress={() => { router.push('/announcement') }} /> } text="Diskusi" onPress={() => { router.push('/discussion?active=true') }} /> - - + } text="Kalender" onPress={() => { router.push('/village-calendar') }} /> } text="Anggota" onPress={() => { router.push('/member') }} /> } text="Jabatan" onPress={() => { router.push('/position') }} /> { @@ -39,7 +38,6 @@ export default function Feature() { (entityUser.role == "supadmin" || entityUser.role == "developer") && <> } text="Lembaga Desa" onPress={() => { router.push('/group') }} /> - {/* } text="Tema" onPress={() => { }} /> */} } text="Banner" onPress={() => { router.push('/banner') }} /> } diff --git a/app/(application)/village-calendar/index.tsx b/app/(application)/village-calendar/index.tsx new file mode 100644 index 0000000..95aa9ea --- /dev/null +++ b/app/(application)/village-calendar/index.tsx @@ -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 ( + + + + {item.timeStart ? ( + + {item.timeStart} - {item.timeEnd} + + ) : ( + + {moment(item.dateStart).format('D MMM YYYY')} – {moment(item.dateEnd).format('D MMM YYYY')} + + )} + + {item.title} + + + + + {item.type === 'calendar' ? 'Divisi' : 'Kegiatan'} + + + + {item.type === 'calendar' ? item.divisionName : item.projectName} + + + + + ); +} + +export default function VillageCalendar() { + const { colors, activeTheme } = useTheme(); + const { token, decryptToken } = useAuthSession(); + const [selected, setSelected] = useState(new Date()); + const [data, setData] = useState([]); + const [dataIndicator, setDataIndicator] = useState<{ calendar: string[], task: string[] }>({ calendar: [], task: [] }); + const [month, setMonth] = useState(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 ( + setSelected(new Date(today))} + /> + ); + }, + IconNext: ( + !loadingBtn && setMonth(month + 1)}> + + + ), + IconPrev: ( + !loadingBtn && setMonth(month - 1)}> + + + ), + }; + + return ( + + ( + router.back()} + /> + ), + }} + /> + + } + style={[Styles.h100]} + > + + {/* Calendar grid */} + + 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 }, + }} + /> + + + {/* Legend */} + + + + Acara Divisi + + + + Kegiatan + + + + {/* Calendar events */} + + Acara Divisi + + {loading ? ( + <> + + + + ) : calendarEvents.length > 0 ? ( + calendarEvents.map((item, index) => ( + handlePressEvent(item)} + /> + )) + ) : ( + + Tidak ada acara + + )} + + + + {/* Task events */} + + Kegiatan + + {loading ? ( + <> + + + + ) : taskEvents.length > 0 ? ( + taskEvents.map((item, index) => ( + handlePressEvent(item)} + /> + )) + ) : ( + + Tidak ada kegiatan + + )} + + + + + + ); +} diff --git a/components/buttonFiturMenu.tsx b/components/buttonFiturMenu.tsx index d9c3533..ba224e6 100644 --- a/components/buttonFiturMenu.tsx +++ b/components/buttonFiturMenu.tsx @@ -14,11 +14,11 @@ export function ButtonFiturMenu({ onPress, icon, text }: Props) { const { colors } = useTheme(); return ( - + {icon} - {text} + {text} ) diff --git a/components/calendar/itemDateCalendar.tsx b/components/calendar/itemDateCalendar.tsx index a932783..a1354f4 100644 --- a/components/calendar/itemDateCalendar.tsx +++ b/components/calendar/itemDateCalendar.tsx @@ -6,19 +6,28 @@ import Text from "../Text"; type Props = { text: string; isSelected: boolean; - isSign: boolean; + isSign?: boolean; + isSignCalendar?: boolean; + isSignTask?: boolean; onPress?: () => void; } - -export default function ItemDateCalendar({ text, isSelected, isSign, onPress }: Props) { +export default function ItemDateCalendar({ text, isSelected, isSign, isSignCalendar, isSignTask, onPress }: Props) { const { colors } = useTheme() + + const showMulti = isSignCalendar !== undefined || isSignTask !== undefined; + return ( - <> - - {text} - - - + + {text} + {showMulti ? ( + + + + + ) : ( + + )} + ) } \ No newline at end of file diff --git a/components/eventItem.tsx b/components/eventItem.tsx index 10117cc..51af2f4 100644 --- a/components/eventItem.tsx +++ b/components/eventItem.tsx @@ -17,16 +17,16 @@ export default function EventItem({ category, title, user, jamAwal, jamAkhir, on const getBackgroundColor = (cat: 'purple' | 'orange') => { if (activeTheme === 'dark') { - return cat === 'orange' ? '#547792' : '#1D546D'; + return cat === 'purple' ? '#2D2B5E' : '#1C3347'; } - return cat === 'orange' ? '#D6E6F2' : '#A9B5DF'; + return cat === 'purple' ? '#A9B5DF' : '#D6E6F2'; }; const getStickColor = (cat: 'purple' | 'orange') => { if (activeTheme === 'dark') { - return cat === 'orange' ? '#94B4C1' : '#5F9598'; + return cat === 'purple' ? '#7886C7' : '#94B4C1'; } - return cat === 'orange' ? '#F5F5F5' : '#7886C7'; + return cat === 'purple' ? '#7886C7' : '#94B4C1'; }; return ( diff --git a/constants/Styles.ts b/constants/Styles.ts index e3b2462..b49195a 100644 --- a/constants/Styles.ts +++ b/constants/Styles.ts @@ -1169,6 +1169,38 @@ const Styles = StyleSheet.create({ fontSize: 10, fontStyle: 'italic', }, + + // village calendar & itemDateCalendar + calendarDotRow: { + flexDirection: 'row', + gap: 2, + height: 6, + marginTop: 1, + }, + calendarDot: { + width: 5, + height: 5, + borderRadius: 3, + }, + villageEventLegendRow: { + marginTop: 10, + marginBottom: 4, + gap: 16, + }, + villageEventLegendItem: { + gap: 6, + }, + villageEventLegendDot: { + width: 10, + height: 10, + borderRadius: 5, + }, + villageEventBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + marginRight: 6, + }, }) export default Styles; \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index cf3e82a..a2a84f0 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -619,6 +619,16 @@ export const apiGetCalendarHistory = async ({ user, search, division, page }: { return response.data; }; +export const apiGetVillageCalendarByDate = async ({ user, date }: { user: string, date: string }) => { + const response = await api.get(`mobile/village-calendar?user=${user}&date=${date}`); + return response.data; +}; + +export const apiGetVillageCalendarIndicator = async ({ user, date }: { user: string, date: string }) => { + const response = await api.get(`mobile/village-calendar/indicator?user=${user}&date=${date}`); + return response.data; +}; + export const apiCreateCalendar = async ({ data }: { data: { idDivision: string, title: string, desc: string, timeStart: string, timeEnd: string, dateStart: string, repeatEventTyper: string, repeatValue: string, linkMeet: string, member: any[], user: string } }) => { const response = await api.post(`/mobile/calendar`, data) return response.data;