6d2711e36e
Default to beenvoice.soconnor.dev with server settings hidden behind Advanced; add Entities tab with clients/businesses, invoice creation, UI fixes for dashboard layout, date fields, FAB position, and card-matched button radius. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.0 KiB
TypeScript
185 lines
5.0 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import DateTimePicker, {
|
|
type DateTimePickerEvent,
|
|
} from "@react-native-community/datetimepicker";
|
|
import { useState } from "react";
|
|
import { Modal, Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
|
|
|
import { fonts, radii, spacing } from "@/constants/theme";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
import { formatDate, formatDateTime } from "@/lib/format";
|
|
|
|
type DateTimeFieldProps = {
|
|
label: string;
|
|
value: Date;
|
|
mode?: "date" | "datetime";
|
|
maximumDate?: Date;
|
|
minimumDate?: Date;
|
|
onChange: (date: Date) => void;
|
|
};
|
|
|
|
export function DateTimeField({
|
|
label,
|
|
value,
|
|
mode = "datetime",
|
|
maximumDate = new Date(),
|
|
minimumDate,
|
|
onChange,
|
|
}: DateTimeFieldProps) {
|
|
const { colors, isDark } = useAppTheme();
|
|
const [open, setOpen] = useState(false);
|
|
const [draft, setDraft] = useState(value);
|
|
|
|
function openPicker() {
|
|
setDraft(value);
|
|
setOpen(true);
|
|
}
|
|
|
|
function applyDate(next: Date) {
|
|
const clamped =
|
|
next.getTime() > maximumDate.getTime()
|
|
? maximumDate
|
|
: minimumDate && next.getTime() < minimumDate.getTime()
|
|
? minimumDate
|
|
: next;
|
|
onChange(clamped);
|
|
}
|
|
|
|
function handleChange(event: DateTimePickerEvent, selected?: Date) {
|
|
if (Platform.OS === "android") {
|
|
setOpen(false);
|
|
if (event.type === "set" && selected) {
|
|
applyDate(selected);
|
|
}
|
|
return;
|
|
}
|
|
if (selected) {
|
|
setDraft(selected);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View style={styles.wrapper}>
|
|
<Text style={[styles.label, { color: colors.mutedForeground }]}>{label}</Text>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
onPress={openPicker}
|
|
style={({ pressed }) => [
|
|
styles.trigger,
|
|
{
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.cardGlass,
|
|
},
|
|
pressed && styles.triggerPressed,
|
|
]}
|
|
>
|
|
<Text style={[styles.value, { color: colors.foreground }]}>
|
|
{mode === "date" ? formatDate(value) : formatDateTime(value)}
|
|
</Text>
|
|
<Ionicons name="calendar-outline" size={18} color={colors.mutedForeground} />
|
|
</Pressable>
|
|
|
|
{Platform.OS === "ios" ? (
|
|
<Modal visible={open} transparent animationType="slide" onRequestClose={() => setOpen(false)}>
|
|
<Pressable style={styles.backdrop} onPress={() => setOpen(false)}>
|
|
<Pressable
|
|
style={[styles.sheet, { backgroundColor: colors.card }]}
|
|
onPress={(event) => event.stopPropagation()}
|
|
>
|
|
<View style={[styles.sheetHeader, { borderBottomColor: colors.border }]}>
|
|
<Pressable onPress={() => setOpen(false)}>
|
|
<Text style={[styles.sheetAction, { color: colors.mutedForeground }]}>Cancel</Text>
|
|
</Pressable>
|
|
<Text style={[styles.sheetTitle, { color: colors.foreground }]}>{label}</Text>
|
|
<Pressable
|
|
onPress={() => {
|
|
applyDate(draft);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<Text style={[styles.sheetAction, { color: colors.primary }]}>Done</Text>
|
|
</Pressable>
|
|
</View>
|
|
<DateTimePicker
|
|
value={draft}
|
|
mode={mode}
|
|
display="spinner"
|
|
maximumDate={maximumDate}
|
|
minimumDate={minimumDate}
|
|
themeVariant={isDark ? "dark" : "light"}
|
|
onChange={handleChange}
|
|
/>
|
|
</Pressable>
|
|
</Pressable>
|
|
</Modal>
|
|
) : open ? (
|
|
<DateTimePicker
|
|
value={draft}
|
|
mode={mode}
|
|
maximumDate={maximumDate}
|
|
minimumDate={minimumDate}
|
|
onChange={handleChange}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
wrapper: {
|
|
gap: spacing.xs,
|
|
alignSelf: "stretch",
|
|
width: "100%",
|
|
},
|
|
label: {
|
|
fontSize: 13,
|
|
fontFamily: fonts.bodyMedium,
|
|
},
|
|
trigger: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
alignSelf: "stretch",
|
|
width: "100%",
|
|
borderWidth: 1,
|
|
borderRadius: radii.lg,
|
|
paddingHorizontal: spacing.md,
|
|
minHeight: 48,
|
|
paddingVertical: spacing.sm,
|
|
},
|
|
triggerPressed: {
|
|
opacity: 0.92,
|
|
},
|
|
value: {
|
|
fontSize: 15,
|
|
fontFamily: fonts.body,
|
|
flex: 1,
|
|
},
|
|
backdrop: {
|
|
flex: 1,
|
|
justifyContent: "flex-end",
|
|
backgroundColor: "rgba(0,0,0,0.45)",
|
|
},
|
|
sheet: {
|
|
borderTopLeftRadius: radii.lg,
|
|
borderTopRightRadius: radii.lg,
|
|
paddingBottom: spacing.lg,
|
|
},
|
|
sheetHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.md,
|
|
borderBottomWidth: 1,
|
|
},
|
|
sheetTitle: {
|
|
fontSize: 15,
|
|
fontFamily: fonts.bodySemiBold,
|
|
},
|
|
sheetAction: {
|
|
fontSize: 15,
|
|
fontFamily: fonts.bodyMedium,
|
|
},
|
|
});
|