Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ MINISTRY_PLATFORM_BASE_URL=https://mpi.ministryplatform.com/ministryplatformapi
MINISTRY_PLATFORM_DEV_CLIENT_ID=
MINISTRY_PLATFORM_DEV_CLIENT_SECRET=

# =============================================================================
# Google Places (optional)
# =============================================================================
# Enables address autocomplete on the Address Line 1 field in the Add/Edit
# Family tool (and any other tool that uses GooglePlacesService).
#
# When unset, the tool falls back to a plain text input — no errors, no UI
# change beyond losing the suggestion dropdown.
#
# Get a key at https://console.cloud.google.com and enable the "Places API
# (New)" — the v1 REST endpoints. Calls go through our server actions, so
# restrict the key by IP / service in Google Cloud Console rather than by
# HTTP referrer.
GOOGLE_PLACES_API_KEY=

# =============================================================================
# NEXT Public Keys
# =============================================================================
Expand Down
14 changes: 14 additions & 0 deletions src/app/(web)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ export default function Home() {
</Link>
</CardContent>
</Card>

<Card className="flex flex-col">
<CardHeader>
<CardTitle>Add/Edit Family</CardTitle>
<CardDescription>
Add/Edit Family tool for Ministry Platform
</CardDescription>
</CardHeader>
<CardContent className="mt-auto">
<Link href="/tools/addeditfamily">
<Button className="w-full">Open Tool</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
);
Expand Down
142 changes: 142 additions & 0 deletions src/app/(web)/tools/addeditfamily/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"use server";

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { FamilyService, PartialSaveError } from "@/services/familyService";
import { GooglePlacesService } from "@/services/googlePlacesService";
import { getCurrentUserIdFromSession } from "@/components/shared-actions/user";
import type {
ContactSearchResult,
FamilyDefaults,
FamilyLookups,
Household,
SaveProgress,
} from "@/lib/dto/family";
import type { PlacePrediction, PlaceDetails } from "@/lib/providers/google-places";

export type ActionError = { success: false; error: string; progress?: SaveProgress };

async function getSession() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) throw new Error("Unauthorized");
return session;
}

export async function searchContacts(term: string): Promise<ContactSearchResult[]> {
await getSession();
const service = await FamilyService.getInstance();
return service.searchContacts(term);
}

export async function fetchFamilyLookups(): Promise<FamilyLookups> {
await getSession();
const service = await FamilyService.getInstance();
return service.getLookups();
}

export async function fetchFamilyDefaults(): Promise<FamilyDefaults> {
await getSession();
const service = await FamilyService.getInstance();
return service.getDefaults();
}

export async function fetchHousehold(
contactId: number,
): Promise<{ success: true; household: Household } | ActionError> {
try {
await getSession();
const service = await FamilyService.getInstance();
const household = await service.getHousehold(contactId);
if (!household) return { success: false, error: "Household not found" };
return { success: true, household };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to load household",
};
}
}

export async function resolveContactIdFromPage(args: {
tableName: string;
primaryKey: string;
recordId: number;
contactIdField: string;
}): Promise<{ success: true; contactId: number | null } | ActionError> {
try {
await getSession();
const service = await FamilyService.getInstance();
const contactId = await service.resolveContactIdFromPage(
args.tableName,
args.primaryKey,
args.recordId,
args.contactIdField,
);
return { success: true, contactId };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to resolve contact",
};
}
}

export async function fetchNextEnvelopeNumber(): Promise<number> {
await getSession();
const service = await FamilyService.getInstance();
return service.getNextEnvelopeNumber();
}

export async function placesEnabled(): Promise<boolean> {
await getSession();
const service = await GooglePlacesService.getInstance();
return service.isEnabled();
}

export async function placeAutocomplete(
input: string,
sessionToken: string,
): Promise<PlacePrediction[]> {
await getSession();
if (input.trim().length < 3) return [];
const service = await GooglePlacesService.getInstance();
if (!service.isEnabled()) return [];
return service.autocomplete(input, sessionToken);
}

export async function placeDetails(
placeId: string,
sessionToken: string,
): Promise<{ success: true; details: PlaceDetails } | ActionError> {
try {
await getSession();
const service = await GooglePlacesService.getInstance();
const details = await service.getPlaceDetails(placeId, sessionToken);
return { success: true, details };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to fetch place details",
};
}
}

export async function saveFamily(
household: Household,
): Promise<{ success: true; progress: SaveProgress } | ActionError> {
try {
const session = await getSession();
const userId = await getCurrentUserIdFromSession(session);
const service = await FamilyService.getInstance();
const progress = await service.saveHousehold(household, userId);
return { success: true, progress };
} catch (error) {
if (error instanceof PartialSaveError) {
return { success: false, error: error.message, progress: error.progress };
}
return {
success: false,
error: error instanceof Error ? error.message : "Failed to save family",
};
}
}
Loading
Loading