15 Commits

Author SHA1 Message Date
6c80eb77fb Merge pull request 'amalia/08-jun-26' (#56) from amalia/08-jun-26 into join
Reviewed-on: #56
2026-06-08 17:27:20 +08:00
ae96a79b69 upd: tampilan jarak antar section pada fitur detail kegiatan dan detail tugas divisi 2026-06-08 17:26:11 +08:00
a5c58d0de2 feat: tambah fitur pilih semua pada halaman pilih anggota divisi 2026-06-08 16:49:16 +08:00
c979a68028 feat: tambah fitur pilih semua pada halaman pilih anggota kegiatan & tugas 2026-06-08 16:43:59 +08:00
2cf5c8d960 feat: tambah fitur pilih semua pada modal pilih divisi
Menambahkan baris "Pilih Semua" / "Batalkan Semua" di atas list pada
ModalSelectMultiple untuk kedua kategori choose-division dan share-division.
2026-06-08 16:26:26 +08:00
9dc4d8dc8d fix: perbaiki parseDate agar case-insensitive sehingga urutan tanggal notifikasi benar 2026-06-08 14:42:27 +08:00
789e4f84f1 fix: kurangi paddingHorizontal card carousel di halaman utama agar label Pengumuman tidak terpotong di iPhone 2026-06-08 12:12:22 +08:00
99c13b57e1 Merge pull request 'amalia/03-jun-26' (#54) from amalia/03-jun-26 into join
Reviewed-on: #54
2026-06-08 11:28:36 +08:00
47ed52e9d2 chore: update kotlin error logs 2026-06-03 13:49:27 +08:00
02904b1e48 chore: bump versionCode 20 → 21 2026-06-03 13:46:59 +08:00
8df5b48578 fix: gunakan tools:node=remove agar Gradle merger hapus READ_MEDIA_IMAGES/VIDEO dari semua library manifest
Filter sebelumnya hanya menghapus dari app manifest, tapi expo-image-picker
menyuntikkan permission lewat library manifest-nya sendiri saat Gradle build.
2026-06-03 13:41:00 +08:00
21617f9c4c chore: update kotlin error logs 2026-06-03 11:32:57 +08:00
383ca069d5 chore: bump versionCode 19 → 20 2026-06-03 11:32:00 +08:00
267454637f fix: hapus READ_MEDIA_IMAGES & VIDEO via Expo config plugin agar tahan prebuild EAS
Perubahan sebelumnya langsung di android/ tidak efektif karena EAS
melakukan prebuild ulang. Plugin withRemoveMediaPermissions.js kini
memfilter permission tersebut saat prebuild berjalan.
2026-06-03 11:08:06 +08:00
f939ddb5f5 Merge pull request 'amalia/02-jun-26' (#53) from amalia/02-jun-26 into join
Reviewed-on: #53
2026-06-02 17:41:33 +08:00
21 changed files with 261 additions and 44 deletions

View File

@@ -1,4 +0,0 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -92,7 +92,7 @@ android {
applicationId 'mobiledarmasaba.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 17
versionCode 21
versionName "2.2.0"
}
signingConfigs {

View File

@@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>

View File

@@ -23,7 +23,7 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 19,
versionCode: 21,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"
@@ -54,6 +54,7 @@ export default {
"expo-font",
"expo-image-picker",
"expo-web-browser",
"./plugins/withRemoveMediaPermissions",
[
"@react-native-firebase/app",
{

View File

@@ -74,6 +74,22 @@ export default function AddMemberTask() {
}
}
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.idUser))
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.idUser)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = availableData
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.idUser))
.map((item: any) => ({ idUser: item.idUser, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
try {
setLoading(true)
@@ -159,6 +175,15 @@ export default function AddMemberTask() {
showsVerticalScrollIndicator={false}
>
{availableData.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
data.map((item: any, index: any) => {

View File

@@ -54,6 +54,21 @@ export default function AddMemberCreateTask() {
}
}
const isAllSelected = data.length > 0 && data.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.idUser)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = data
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.idUser))
.map((item: any) => ({ idUser: item.idUser, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
try {
dispatch(setMemberChoose(selectMember))
@@ -127,6 +142,15 @@ export default function AddMemberCreateTask() {
showsVerticalScrollIndicator={false}
>
{data.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
data.map((item: any, index: any) => {

View File

@@ -77,6 +77,22 @@ export default function AddMemberDivision() {
}
}
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.id))
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.id)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = availableData
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
try {
setLoading(true)
@@ -141,7 +157,7 @@ export default function AddMemberDivision() {
selectMember.length > 0
?
<View>
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
<ScrollView horizontal style={[Styles.mb05, Styles.pv10]} showsHorizontalScrollIndicator={false}>
{
selectMember.map((item: any, index: any) => (
<ImageWithLabel
@@ -162,6 +178,15 @@ export default function AddMemberDivision() {
showsVerticalScrollIndicator={false}
>
{availableData.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
data.map((item: any, index: any) => {

View File

@@ -9,8 +9,8 @@ import Styles from "@/constants/Styles";
import { apiGetUser } from "@/lib/api";
import { setFormCreateDivision } from "@/lib/divisionCreate";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, ScrollView, View } from "react-native";
@@ -55,6 +55,21 @@ export default function CreateDivisionAddMember() {
}
}
const isAllSelected = data.length > 0 && data.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.id)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = data
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
dispatch(setFormCreateDivision({ ...update, member: selectMember }))
router.push(`./add-admin-division`)
@@ -93,7 +108,7 @@ export default function CreateDivisionAddMember() {
selectMember.length > 0
?
<View>
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
<ScrollView horizontal style={[Styles.mb05, Styles.pv10]} showsHorizontalScrollIndicator={false}>
{
selectMember.map((item: any, index: any) => (
<ImageWithLabel
@@ -114,6 +129,15 @@ export default function CreateDivisionAddMember() {
showsVerticalScrollIndicator={false}
>
{data.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
data.map((item: any, index: any) => {

View File

@@ -84,7 +84,7 @@ export default function Notification() {
}
const parseDate = (str: string) => {
const [d, m, y] = str.split(' ')
return new Date(Number(y), BULAN[m] ?? 0, Number(d)).getTime()
return new Date(Number(y), BULAN[m?.toUpperCase()] ?? 0, Number(d)).getTime()
}
const groups: Record<string, Props[]> = {}

View File

@@ -78,6 +78,22 @@ export default function AddMemberProject() {
}
}
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.id))
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.id)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = availableData
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
try {
setLoading(true)
@@ -160,6 +176,15 @@ export default function AddMemberProject() {
showsVerticalScrollIndicator={false}
style={[Styles.h100, { backgroundColor: colors.background }]}
>
{availableData.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
<View style={[Styles.mb100]}>

View File

@@ -61,6 +61,21 @@ export default function AddMemberCreateProject() {
}
}
const isAllSelected = data.length > 0 && data.every((item: any) =>
selectMember.some((s: any) => s.idUser == item.id)
)
function handleSelectAll() {
if (isAllSelected) {
setSelectMember([])
} else {
const newMembers = data
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
setSelectMember([...selectMember, ...newMembers])
}
}
async function handleAddMember() {
try {
dispatch(setMemberChoose(selectMember))
@@ -134,6 +149,15 @@ export default function AddMemberCreateProject() {
style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}
>
{data.length > 0 && (
<Pressable
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
onPress={handleSelectAll}
>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
</Pressable>
)}
{
data.length > 0 ?
data.map((item: any, index: any) => {

View File

@@ -67,9 +67,8 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
<View
style={[
Styles.mv15,
Styles.p15,
Styles.round05,
{ backgroundColor: colors.card },
{ backgroundColor: colors.card, paddingVertical: 15, paddingHorizontal: 10 },
Styles.wrapHomeCarousel
]}
>

View File

@@ -97,7 +97,7 @@ export default function ItemSectionTanggalTugas({ status, title, dateStart, date
borderWidth: 1,
borderColor: colors.icon + '18',
backgroundColor: colors.card,
marginBottom: 10,
marginBottom: 0,
}}
>
{/* Accent bar kiri */}

View File

@@ -128,6 +128,35 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
};
const groupsWithDivisions = data.filter((group: any) => group.Division?.length > 0)
const isAllSelected = category === 'choose-division'
? groupsWithDivisions.length > 0 && groupsWithDivisions.every((group: any) =>
checked[group.id]?.length === group.Division?.length
)
: data.length > 0 && selectedDivision.length === data.length
const handleSelectAll = () => {
if (category === 'choose-division') {
if (isAllSelected) {
setChecked({})
} else {
const newChecked: CheckedState = {}
data.forEach((group: any) => {
if (group.Division?.length > 0) {
newChecked[group.id] = group.Division.map((d: any) => d.id)
}
})
setChecked(newChecked)
}
} else {
if (isAllSelected) {
setSelectedDivision([])
} else {
setSelectedDivision(data.map((d: any) => ({ id: d.id, name: d.name })))
}
}
}
const handleSubmit = () => {
if (category == "choose-division") {
const selectedGroups: GroupData[] = [];
@@ -154,25 +183,32 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
{
category == 'share-division' ?
<>
{
data.map((item: any, index: number) => {
return (
<Pressable key={index} style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => {
handleDivisionClick(index)
}}>
<Text numberOfLines={1} style={[Styles.w80]}>{item.name}</Text>
{
selectedDivision.some((i: any) => i.id == item.id)
? <AntDesign name="check" size={17} color={colors.text} />
: <></>
}
</Pressable>
)
})
}
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={handleSelectAll}>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={17} color={colors.text} />}
</Pressable>
{data.map((item: any, index: number) => {
return (
<Pressable key={index} style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => {
handleDivisionClick(index)
}}>
<Text numberOfLines={1} style={[Styles.w80]}>{item.name}</Text>
{
selectedDivision.some((i: any) => i.id == item.id)
? <AntDesign name="check" size={17} color={colors.text} />
: <></>
}
</Pressable>
)
})}
</>
:
data.map((item: any, index: number) => {
<>
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={handleSelectAll}>
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
{isAllSelected && <AntDesign name="check" size={17} color={colors.text} />}
</Pressable>
{data.map((item: any, index: number) => {
return (
<View key={index}>
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => { handleGroupCheck(item.id) }}>
@@ -199,7 +235,8 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
}
</View>
)
})
})}
</>
}
</ScrollView>
<View style={[Styles.absolute0, { width: '100%' }]}>
@@ -207,4 +244,4 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
</View>
</DrawerBottom>
)
}
}

View File

@@ -127,7 +127,7 @@ export default function SectionFile({ status, member, refreshing }: { status: nu
<>
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>File</Text>
{loading ? (
<View style={Styles.fileGrid}>

View File

@@ -74,7 +74,7 @@ export default function SectionMember({ status, refreshing }: { status: number |
return (
<>
<View style={[Styles.mb15]}>
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
<View style={[Styles.rowSpaceBetween, { marginBottom: 5 }]}>
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
{!loading && data.length > 0 && (
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data.length} orang</Text>

View File

@@ -174,9 +174,9 @@ export default function SectionTanggalTugasProject({ status, member, refreshing,
return (
<>
<View style={[Styles.mb15, Styles.mt10]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
<View>
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>Tanggal & Tugas</Text>
<View style={{ gap: 10 }}>
{loading
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
: data.length > 0

View File

@@ -153,7 +153,7 @@ export default function SectionFileTask({ refreshing, isMemberDivision }: { refr
<>
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>File</Text>
{loading ? (
<View style={Styles.fileGrid}>
{arrSkeleton.map((_, index) => (

View File

@@ -100,7 +100,7 @@ export default function SectionMemberTask({ refreshing, isAdminDivision }: { ref
return (
<>
<View style={[Styles.mb15]}>
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
<View style={[Styles.rowSpaceBetween, { marginBottom: 5 }]}>
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
{!loading && data.length > 0 && (
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data.length} orang</Text>

View File

@@ -172,9 +172,9 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision,
return (
<>
<View style={[Styles.mb15, Styles.mt10]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
<View>
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>Tanggal & Tugas</Text>
<View style={{ gap: 10 }}>
{loading
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
: data.length > 0

View File

@@ -0,0 +1,37 @@
const { withAndroidManifest } = require('@expo/config-plugins');
const BLOCKED_PERMISSIONS = [
'android.permission.READ_MEDIA_IMAGES',
'android.permission.READ_MEDIA_VIDEO',
];
const withRemoveMediaPermissions = (config) =>
withAndroidManifest(config, (config) => {
const manifest = config.modResults.manifest;
// Pastikan xmlns:tools ada di manifest root
if (!manifest.$['xmlns:tools']) {
manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools';
}
// Hapus entry yang ada (apapun atributnya)
const existing = manifest['uses-permission'] ?? [];
manifest['uses-permission'] = existing.filter(
(perm) => !BLOCKED_PERMISSIONS.includes(perm.$?.['android:name'])
);
// Tambahkan entry dengan tools:node="remove" agar Gradle merger
// membuang permission ini dari SEMUA sumber (termasuk library manifests)
for (const permission of BLOCKED_PERMISSIONS) {
manifest['uses-permission'].push({
$: {
'android:name': permission,
'tools:node': 'remove',
},
});
}
return config;
});
module.exports = withRemoveMediaPermissions;