upd: announcement

Deskripsi:
- load all announcement
- pencarian announcement
- tambah announcement
- get detail one announcement

No Issues
This commit is contained in:
amel
2025-05-07 17:26:49 +08:00
parent dee684fb88
commit 6cae53cbc7
8 changed files with 431 additions and 153 deletions

View File

@@ -1,12 +1,40 @@
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
import ButtonBackHeader from "@/components/buttonBackHeader";
import Styles from "@/constants/Styles";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import { apiGetAnnouncementOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign, Entypo, MaterialIcons } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { SafeAreaView, ScrollView, SectionList, Text, View } from "react-native";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, Text, View } from "react-native";
type Props = {
id: string
title: string
desc: string
}
export default function DetailAnnouncement() {
const { id } = useLocalSearchParams()
const { id } = useLocalSearchParams<{ id: string }>();
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props>()
const [dataMember, setDataMember] = useState<any>({})
async function handleLoad() {
try {
const hasil = await decryptToken(String(token?.current))
const response = await apiGetAnnouncementOne({ id: id, user: hasil })
setData(response.data)
setDataMember(response.member)
} catch (error) {
console.error(error)
}
}
useEffect(() => {
handleLoad()
}, [])
return (
<SafeAreaView>
<Stack.Screen
@@ -17,39 +45,42 @@ export default function DetailAnnouncement() {
headerRight: () => <HeaderRightAnnouncementDetail id={id} />,
}}
/>
{/* <ScrollView> */}
<View style={[Styles.p15]}>
<View style={[Styles.wrapPaper]}>
<View style={Styles.rowItemsCenter}>
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefaultSemiBold]}>Libur Nyepi</Text>
<ScrollView>
<View style={[Styles.p15]}>
<View style={[Styles.wrapPaper]}>
<View style={Styles.rowItemsCenter}>
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefaultSemiBold]}>{data?.title}</Text>
</View>
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
<AntDesign name="profile" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefault]}>{data?.desc}</Text>
</View>
</View>
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
<AntDesign name="profile" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefault]}>Pengumuman terkait libur nyepi</Text>
<View style={[Styles.wrapPaper, Styles.mv15]}>
{
Object.keys(dataMember).map((v: any, i: any) => {
return (
<View key={i} style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
{
dataMember[v].map((item: any, x: any) => {
return (
<View style={[Styles.rowItemsCenter]}>
<Entypo name="dot-single" size={24} color="black" />
<Text style={[Styles.textDefault]}>{item.division}</Text>
</View>
)
})
}
</View>
)
})
}
</View>
</View>
<View style={[Styles.wrapPaper, Styles.mv15, { paddingTop: 0, paddingBottom: 10 }]}>
<SectionList
sections={[
{ title: 'Dinas', data: ['TU dan Umum', 'Kasi Pemerintahan', 'Pelaksana Kewilayahan'] },
{
title: 'Adat',
data: [
'PKK',
'Karang Taruna',
],
},
]}
renderItem={({ item }) => <Text style={Styles.textDefault}>{item}</Text>}
renderSectionHeader={({ section }) => (
<Text style={[Styles.textDefaultSemiBold, Styles.mt10, { borderBottomWidth: 1, borderBottomColor: 'gray' }]}>{section.title}</Text>
)}
keyExtractor={item => `basicListEntry-${item}`}
/>
</View>
</View>
{/* </ScrollView> */}
</ScrollView>
</SafeAreaView>
)
}

View File

@@ -2,43 +2,172 @@ import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import { InputForm } from "@/components/inputForm";
import ModalSelectMultiple from "@/components/modalSelectMultiple";
import Styles from "@/constants/Styles";
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
import { apiCreateAnnouncement } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { Entypo } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import { SafeAreaView, ScrollView, ToastAndroid, View } from "react-native";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, Text, ToastAndroid, View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
export default function CreateAnnouncement() {
const dispatch = useDispatch()
const update = useSelector((state: any) => state.announcementUpdate)
const { token, decryptToken } = useAuthSession()
const [disableBtn, setDisableBtn] = useState(true);
const [modalDivisi, setModalDivisi] = useState(false);
const [divisionMember, setDivisionMember] = useState<any>([]);
const [dataForm, setDataForm] = useState({
title: "",
desc: "",
});
const [error, setError] = useState({
title: false,
desc: false,
});
function validationForm(cat: string, val: any) {
if (cat == "title") {
setDataForm({ ...dataForm, title: val });
if (val == "" || val == "null") {
setError({ ...error, title: true });
} else {
setError({ ...error, title: false });
}
} else if (cat == "desc") {
setDataForm({ ...dataForm, desc: val });
if (val == "" || val == "null") {
setError({ ...error, desc: true });
} else {
setError({ ...error, desc: false });
}
}
}
function checkForm() {
if (
Object.values(error).some((v) => v == true) ||
Object.values(dataForm).some((v) => v == "")
) {
setDisableBtn(true);
} else {
setDisableBtn(false);
}
}
useEffect(() => {
checkForm();
}, [error, dataForm]);
async function handleCreate() {
try {
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateAnnouncement({
data: { ...dataForm, user: hasil, groups: divisionMember },
});
if (response.success) {
dispatch(setUpdateAnnouncement(!update))
ToastAndroid.show("Berhasil menambahkan data", ToastAndroid.SHORT);
router.back();
}
} catch (error) {
console.error(error);
}
}
return (
<SafeAreaView>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Tambah Pengumuman',
headerTitleAlign: 'center',
headerRight: () => <ButtonSaveHeader category="create" onPress={() => {
ToastAndroid.show('Berhasil menambah data', ToastAndroid.SHORT)
router.push('/announcement')
}} />
headerLeft: () => (
<ButtonBackHeader
onPress={() => {
router.back();
}}
/>
),
headerTitle: "Tambah Pengumuman",
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn}
category="create"
onPress={() => {
divisionMember.length == 0
? ToastAndroid.show("Anda belum memilih divisi", ToastAndroid.SHORT)
: handleCreate();
}}
/>
),
}}
/>
<ScrollView>
<View style={[Styles.p15, Styles.mb100]}>
<InputForm label="Judul" type="default" placeholder="Judul Pengumuman" required />
<InputForm label="Pengumuman" type="default" placeholder="Deskripsi Pengumuman" required />
<ButtonSelect value="Pilih divisi penerima pengumuman" />
{/* <ButtonForm
text="SIMPAN"
<InputForm
label="Judul"
type="default"
placeholder="Judul Pengumuman"
required
error={error.title}
errorText="Judul harus diisi"
onChange={(val) => validationForm("title", val)}
/>
<InputForm
label="Pengumuman"
type="default"
placeholder="Deskripsi Pengumuman"
required
error={error.desc}
errorText="Pengumuman harus diisi"
onChange={(val) => validationForm("desc", val)}
/>
<ButtonSelect
value="Pilih divisi penerima pengumuman"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menambahkan data?',
onPress: () => {
ToastAndroid.show('Berhasil menambahkan data', ToastAndroid.SHORT)
router.push('/announcement')
}
})
}} /> */}
setModalDivisi(true)
}}
/>
{
divisionMember.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10]}>
{
divisionMember.map((item: { name: any; Division: any }, index: any) => {
return (
<View>
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
{
item.Division.map((division: any) => (
<View style={[Styles.rowItemsCenter, Styles.mv05]}>
<Entypo name="dot-single" size={24} color="black" />
<Text style={[Styles.textDefault]}>{division.name}</Text>
</View>
))
}
</View>
)
})
}
</View>
}
</View>
</ScrollView>
<ModalSelectMultiple
choose="dinas"
title="Pilih Divisi"
category="choose-division"
open={modalDivisi}
close={setModalDivisi}
onSelect={(val) => {
setDivisionMember(val)
setModalDivisi(false)
}}
/>
</SafeAreaView>
)
}
);
}

View File

@@ -4,11 +4,43 @@ import ButtonBackHeader from "@/components/buttonBackHeader";
import InputSearch from "@/components/inputSearch";
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import { apiGetAnnouncement } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { MaterialIcons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
import { SafeAreaView, ScrollView, View } from "react-native";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, Text, View } from "react-native";
import { useSelector } from "react-redux";
type Props = {
id: string,
title: string,
desc: string,
createdAt: string
}
export default function Announcement() {
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('')
const entityUser = useSelector((state: any) => state.user)
const update = useSelector((state: any) => state.announcementUpdate)
async function handleLoad() {
try {
const hasil = await decryptToken(String(token?.current))
const response = await apiGetAnnouncement({ user: hasil, search: search })
setData(response.data)
} catch (error) {
console.error(error)
}
}
useEffect(() => {
handleLoad()
}, [search, update])
return (
<SafeAreaView>
<Stack.Screen
@@ -16,97 +48,36 @@ export default function Announcement() {
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightAnnouncementList />
headerRight: () => entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementList /> : <></>
}}
/>
<ScrollView>
<View style={[Styles.p15, Styles.mb100]}>
<InputSearch />
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<BorderBottomItem
onPress={() => {router.push('/announcement/7493')}}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title="Libur Nyepi"
desc="Pengumuman terkait libur nyepi"
rightTopInfo="23 Mar 2025"
/>
<InputSearch onChange={setSearch} />
{
data.length > 0
?
data.map((item, index) => {
return (
<BorderBottomItem
key={index}
onPress={() => { router.push(`/announcement/${item.id}`) }}
borderType="bottom"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
<MaterialIcons name="campaign" size={25} color={'#384288'} />
</View>
}
title={item.title}
desc={item.desc}
rightTopInfo={item.createdAt}
/>
)
})
:
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada pengumuman</Text>
}
</View>
</ScrollView>
</SafeAreaView>

View File

@@ -1,7 +1,9 @@
import Styles from "@/constants/Styles"
import { apiGetDivisionGroup } from "@/lib/api"
import { useAuthSession } from "@/providers/AuthProvider"
import { AntDesign } from "@expo/vector-icons"
import { useState } from "react"
import { Pressable, Text, View } from "react-native"
import { useEffect, useState } from "react"
import { Pressable, ScrollView, Text, ToastAndroid, View } from "react-native"
import { ButtonForm } from "./buttonForm"
import DrawerBottom from "./drawerBottom"
@@ -9,17 +11,97 @@ type Props = {
open: boolean
close: (value: boolean) => void
title: string
category: 'share-division' | 'status-task'
category: 'share-division' | 'choose-division'
choose: string
onSelect: (value: { val: string, label: string }[]) => void
onSelect: (value: any[]) => void
}
type CheckedState = {
[key: string]: string[];
}
type GroupData = {
id: string;
name: string;
Division: {
id: string;
name: string;
}[];
}
export default function ModalSelectMultiple({ open, close, title, category, choose, onSelect }: Props) {
const [isChoose, setChoose] = useState(choose)
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<any>([])
const [checked, setChecked] = useState<CheckedState>({});
async function handleLoadChooseDivision() {
try {
const hasil = await decryptToken(String(token?.current))
const response = await apiGetDivisionGroup({ user: hasil })
setData(response.data)
} catch (error) {
console.error(error)
}
}
useEffect(() => {
if (category == 'choose-division') {
handleLoadChooseDivision()
} else if (category == 'share-division') {
// handleLoadPosition()
}
}, [open]);
const handleCheck = (groupId: string, divisionId: string) => {
const newChecked = { ...checked };
if (newChecked[groupId]) {
if (newChecked[groupId].includes(divisionId)) {
newChecked[groupId] = newChecked[groupId].filter(item => item !== divisionId);
if (newChecked[groupId].length === 0) {
delete newChecked[groupId];
}
} else {
newChecked[groupId].push(divisionId);
}
} else {
newChecked[groupId] = [divisionId];
}
setChecked(newChecked);
};
const handleGroupCheck = (groupId: string) => {
const newChecked = { ...checked };
if (newChecked[groupId]) {
delete newChecked[groupId];
} else {
if (data.find((item: { id: string }) => item.id === groupId)?.Division.length == 0) {
return ToastAndroid.show('Tidak ada divisi', ToastAndroid.SHORT)
}
newChecked[groupId] = data.find((item: { id: string }) => item.id === groupId)?.Division.map((item: { id: any }) => item.id) || [];
}
setChecked(newChecked);
};
const handleSubmit = () => {
const selectedGroups: GroupData[] = [];
Object.keys(checked).forEach((groupId) => {
const group = data.find((item: { id: string }) => item.id === groupId);
if (group) {
selectedGroups.push({
id: group.id,
name: group.name,
Division: group.Division.filter((division: { id: string }) => checked[groupId].includes(division.id)),
});
}
});
onSelect(selectedGroups);
};
return (
<DrawerBottom animation="slide" isVisible={open} setVisible={close} title={title} height={75}>
<View>
<ScrollView style={[Styles.mb50]}>
{
category == 'share-division' ?
<>
@@ -41,11 +123,38 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
</Pressable>
</>
:
<></>
data.map((item: any, index: number) => {
return (
<>
<Pressable key={index} style={[Styles.itemSelectModal]} onPress={() => { handleGroupCheck(item.id) }}>
<Text style={[Styles.textMediumSemiBold]}>{item.name}</Text>
{
checked[item.id] && checked[item.id].length === item.Division.length
? <AntDesign name="check" size={20} />
: (checked[item.id] && checked[item.id].length > 0 && checked[item.id].length < item.Division.length)
? <AntDesign name="minus" size={20} />
: <></>
}
</Pressable>
{
item.Division.map((child: any, v: number) => {
return (
<Pressable key={v} style={[Styles.itemSelectModal]} onPress={() => { handleCheck(item.id, child.id) }}>
<Text style={[Styles.ml10, Styles.textMediumNormal]}>{child.name}</Text>
{
checked[item.id] && checked[item.id].includes(child.id) && <AntDesign name="check" size={20} />
}
</Pressable>
)
})
}
</>
)
})
}
</View>
</ScrollView>
<View style={[Styles.absolute0, { width: '100%' }]}>
<ButtonForm text="Simpan" onPress={() => { onSelect([{ val: 'dinas', label: 'Dinas' }]) }} />
<ButtonForm text="PILIH" onPress={() => { handleSubmit() }} />
</View>
</DrawerBottom>
)

View File

@@ -74,6 +74,9 @@ const Styles = StyleSheet.create({
mb30: {
marginBottom: 30
},
mb50: {
marginBottom: 50
},
mb100: {
marginBottom: 100
},

14
lib/announcementUpdate.ts Normal file
View File

@@ -0,0 +1,14 @@
import { createSlice } from '@reduxjs/toolkit';
const announcementUpdate = createSlice({
name: 'announcementUpdate',
initialState: false,
reducers: {
setUpdateAnnouncement: (state, action) => {
return action.payload;
},
},
});
export const { setUpdateAnnouncement } = announcementUpdate.actions;
export default announcementUpdate.reducer;

View File

@@ -203,3 +203,22 @@ export const apiAddMemberDiscussionGeneral = async ({ data, id }: { data: { user
return response.data;
};
export const apiGetAnnouncement = async ({ user, search }: { user: string, search: string }) => {
const response = await api.get(`mobile/announcement?user=${user}&search=${search}`);
return response.data;
};
export const apiGetDivisionGroup = async ({ user }: { user: string }) => {
const response = await api.get(`mobile/group/get-division?user=${user}`);
return response.data;
};
export const apiCreateAnnouncement = async ({ data }: { data: { title: string, desc: string, user: string, groups: any[] } }) => {
const response = await api.post(`/mobile/announcement/`, data)
return response.data;
};
export const apiGetAnnouncementOne = async ({ user, id }: { user: string, id: string }) => {
const response = await api.get(`mobile/announcement/${id}?user=${user}`);
return response.data;
};

View File

@@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit';
import announcementUpdate from './announcementUpdate';
import bannerReducer from './bannerSlice';
import discussionGeneralDetailUpdate from './discussionGeneralDetail';
import entitiesReducer from './entitiesSlice';
@@ -19,7 +20,8 @@ const store = configureStore({
memberUpdate: memberUpdate,
filterGroup: filterSlice,
discussionGeneralDetailUpdate: discussionGeneralDetailUpdate,
memberChoose: memberChoose
memberChoose: memberChoose,
announcementUpdate: announcementUpdate
}
});