diff --git a/babel-translations.babel b/babel-translations.babel index 11c2b9d..1fdf8d4 100644 --- a/babel-translations.babel +++ b/babel-translations.babel @@ -639,6 +639,27 @@ + + status + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + diff --git a/components/global-search/global-search.jsx b/components/global-search/global-search.jsx index 645d520..0de24db 100644 --- a/components/global-search/global-search.jsx +++ b/components/global-search/global-search.jsx @@ -3,7 +3,7 @@ import { useLocalSearchParams } from "expo-router"; import debounce from "lodash/debounce"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { FlatList, View } from "react-native"; +import { FlatList, KeyboardAvoidingView, Platform } from "react-native"; import { ActivityIndicator, Icon, Text } from "react-native-paper"; import env from "../../env"; import ErrorDisplay from "../error/error-display"; @@ -40,7 +40,7 @@ export default function GlobalSearch() { const [error, setError] = useState(null); const [results, setResults] = useState([]); const { t } = useTranslation(); - // Placeholder: Replace with actual API call (e.g., Apollo client query, REST fetch, Redux saga dispatch) + const performSearch = useCallback(async (query) => { // Defensive trimr const q = (query || "").trim(); @@ -57,7 +57,7 @@ export default function GlobalSearch() { ?.filter((hit) => hit._index === "jobs") .map((hit) => hit._source); - setResults(jobResults); + setResults([...jobResults, { id: "footer-spacer", height: 128 }]); } else { setError("No results available. Try again."); } @@ -79,23 +79,35 @@ export default function GlobalSearch() { if (globalSearch === undefined || globalSearch.trim() === "") { return ( - + {t("globalsearch.labels.entersearch")} - + ); } return ( - - {loading && } + + {loading && } {error && } - - {results.length} results found - + {!loading && ( + + {results.length} results found + + )} item.id?.toString()} renderItem={(object) => } /> - + ); } diff --git a/components/job-notes/job-notes.jsx b/components/job-notes/job-notes.jsx index e76a14f..f952230 100644 --- a/components/job-notes/job-notes.jsx +++ b/components/job-notes/job-notes.jsx @@ -3,10 +3,16 @@ import { useQuery } from "@apollo/client"; import { AntDesign } from "@expo/vector-icons"; import { useGlobalSearchParams } from "expo-router"; import { DateTime } from "luxon"; -import React, { useCallback, useState } from "react"; +import React, { memo, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { FlatList, RefreshControl, View } from "react-native"; -import { ActivityIndicator, Button, Card, Text } from "react-native-paper"; +import { + ActivityIndicator, + Button, + Card, + Divider, + Text, +} from "react-native-paper"; import ErrorDisplay from "../error/error-display"; import NewNoteModal from "./new-note-modal"; @@ -33,17 +39,41 @@ export default function JobNotes() { return refetch(); }; + const job = data?.jobs_by_pk; + + // Memoized list data (only when job & notes exist) + const listData = useMemo(() => { + if (!job?.notes) return []; + const notes = job.notes; + const pinnedNotes = []; + const otherNotes = []; + for (const n of notes) { + (n.pinned ? pinnedNotes : otherNotes).push(n); + } + pinnedNotes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + otherNotes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + const DIVIDER_ID = "__divider__"; + return pinnedNotes.length > 0 && otherNotes.length > 0 + ? [...pinnedNotes, { id: DIVIDER_ID, type: "divider" }, ...otherNotes] + : [...pinnedNotes, ...otherNotes]; + }, [job?.notes]); + + const keyExtractor = useCallback((item) => item.id, []); + const renderItem = useCallback( + ({ item }) => + item.type === "divider" ? : , + [] + ); + if (loading) { return ; } if (error) { return ; } - - if (!data?.jobs_by_pk) { + if (!job) { return ; } - const job = data.jobs_by_pk; return ( @@ -55,7 +85,7 @@ export default function JobNotes() { > {t("jobdetail.actions.addnote")} - {job.notes.length === 0 ? ( + {listData.length === 0 ? ( {t("jobdetail.labels.nojobnotes")} @@ -67,8 +97,13 @@ export default function JobNotes() { } style={{ flex: 1 }} - data={job.notes} - renderItem={(object) => } + data={listData} + keyExtractor={keyExtractor} + renderItem={renderItem} + removeClippedSubviews + initialNumToRender={12} + maxToRenderPerBatch={8} + windowSize={7} /> )} @@ -90,26 +125,20 @@ function NoteListItem({ item }) { {item.text} + {item.pinned && ( + + )} {item.private && ( - + )} {item.critical && ( - + )} {item.created_by} @@ -122,4 +151,8 @@ function NoteListItem({ item }) { ); -} +}); + +const DividerItem = memo(function DividerItem() { + return ; +}); diff --git a/components/job-notes/new-note-modal.jsx b/components/job-notes/new-note-modal.jsx index f3ce601..720332e 100644 --- a/components/job-notes/new-note-modal.jsx +++ b/components/job-notes/new-note-modal.jsx @@ -124,7 +124,7 @@ const NewNoteModal = ({ type: values.type, created_by: currentUser?.email, }; - console.log("*** ~ handleSubmit ~ noteInput:", noteInput); + // TODO: If backend supports attaching note to multiple related ROs, perform additional mutation(s) // here after creating the base note. This may involve an insert into a join table like note_jobs. // values.relatedros contains boolean flags keyed by job id. @@ -272,7 +272,12 @@ const NewNoteModal = ({ { paddingBottom: Math.max(insets.bottom, 12) }, ]} > -