Add church-registered flag + fix country search

- Pastors: churchRegistered boolean; the registration code field now shows
  only when the church is registered (admin condition), and is cleared when
  the flag is turned off
- Frontend modal: "Iglesia registrada" toggle reveals a conditional
  "Código de registro" input; two new toggleable table columns
- Search: country is stored as an ISO code, so a country typed by name (e.g.
  "venezuela", accent-insensitive) now resolves to its code(s) and matches

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Julio Ruiz 2026-08-14 01:17:06 -05:00
parent efeee14752
commit ba08fc087b
5 changed files with 107 additions and 1 deletions

View File

@ -18,6 +18,8 @@ export type PastorRow = {
countryName: string | null
countryFlag: string
impactedPeople: number | null
churchRegistered: boolean
registrationNumber: string | null
inTelegram: boolean
notes: string | null
letterUrl: string | null
@ -36,6 +38,8 @@ type ColumnKey =
| 'state'
| 'country'
| 'impactedPeople'
| 'churchRegistered'
| 'registrationNumber'
| 'inTelegram'
| 'letter'
@ -122,6 +126,22 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
cell: (p) =>
p.impactedPeople === null ? dash : p.impactedPeople.toLocaleString('es'),
},
{
key: 'churchRegistered',
label: 'Iglesia registrada',
align: 'center',
cell: (p) =>
p.churchRegistered ? (
<span className="badge badge-success badge-sm"></span>
) : (
<span className="badge badge-ghost badge-sm">No</span>
),
},
{
key: 'registrationNumber',
label: 'Código de registro',
cell: (p) => <span className="whitespace-nowrap">{p.registrationNumber || dash}</span>,
},
{
key: 'inTelegram',
label: 'Telegram',
@ -343,6 +363,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
const [impactedPeople, setImpactedPeople] = useState(
pastor?.impactedPeople != null ? String(pastor.impactedPeople) : '',
)
const [churchRegistered, setChurchRegistered] = useState(pastor?.churchRegistered ?? false)
const [registrationNumber, setRegistrationNumber] = useState(pastor?.registrationNumber ?? '')
const [inTelegram, setInTelegram] = useState(pastor?.inTelegram ?? false)
const [notes, setNotes] = useState(pastor?.notes ?? '')
@ -365,6 +387,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
state,
country,
impactedPeople: impactedPeople.trim() === '' ? null : Number(impactedPeople),
churchRegistered,
registrationNumber: churchRegistered ? registrationNumber : null,
inTelegram,
notes,
}
@ -510,6 +534,32 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
onChange={(e) => setImpactedPeople(e.target.value)}
/>
<span className="text-sm font-medium sm:text-right sm:pr-1">Iglesia registrada</span>
<input
type="checkbox"
className="toggle toggle-success"
checked={churchRegistered}
onChange={(e) => setChurchRegistered(e.target.checked)}
/>
{churchRegistered && (
<>
<label
htmlFor="f-regcode"
className="text-sm font-medium sm:text-right sm:pr-1"
>
Código de registro
</label>
<input
id="f-regcode"
className="input input-bordered w-full"
value={registrationNumber}
placeholder="Código oficial de registro"
onChange={(e) => setRegistrationNumber(e.target.value)}
/>
</>
)}
<span className="text-sm font-medium sm:text-right sm:pr-1">En Telegram</span>
<input
type="checkbox"

View File

@ -15,6 +15,8 @@ export type PastorUpdateInput = {
state?: string | null
country?: string | null
impactedPeople?: number | null
churchRegistered?: boolean
registrationNumber?: string | null
inTelegram?: boolean
notes?: string | null
}
@ -42,6 +44,9 @@ export async function createPastor(data: PastorUpdateInput) {
data.impactedPeople === null || data.impactedPeople === undefined
? undefined
: Number(data.impactedPeople),
churchRegistered: Boolean(data.churchRegistered),
// Clear the code when the church isn't marked as registered.
registrationNumber: data.churchRegistered ? data.registrationNumber || undefined : null,
inTelegram: Boolean(data.inTelegram),
notes: data.notes || undefined,
},
@ -71,6 +76,9 @@ export async function updatePastor(id: number, data: PastorUpdateInput) {
data.impactedPeople === null || data.impactedPeople === undefined
? undefined
: Number(data.impactedPeople),
churchRegistered: Boolean(data.churchRegistered),
// Clear the code when the church isn't marked as registered.
registrationNumber: data.churchRegistered ? data.registrationNumber || undefined : null,
inTelegram: Boolean(data.inTelegram),
notes: data.notes || undefined,
},

View File

@ -30,6 +30,32 @@ const flag = (code?: string | null): string => {
return String.fromCodePoint(base + (cc.charCodeAt(0) - 65), base + (cc.charCodeAt(1) - 65))
}
// Accent/case-insensitive normalizer for search matching ("méxico" ~ "mexico").
const normalize = (s: string): string =>
s
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toLowerCase()
.trim()
// All ISO 3166-1 alpha-2 codes, so a country typed by name resolves to its code
// (country is stored as the ISO code, e.g. "MX" for México).
// prettier-ignore
const ALPHA2 = ['AD','AE','AF','AG','AI','AL','AM','AO','AQ','AR','AS','AT','AU','AW','AX','AZ','BA','BB','BD','BE','BF','BG','BH','BI','BJ','BL','BM','BN','BO','BQ','BR','BS','BT','BV','BW','BY','BZ','CA','CC','CD','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CW','CX','CY','CZ','DE','DJ','DK','DM','DO','DZ','EC','EE','EG','EH','ER','ES','ET','FI','FJ','FK','FM','FO','FR','GA','GB','GD','GE','GF','GG','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GW','GY','HK','HM','HN','HR','HT','HU','ID','IE','IL','IM','IN','IO','IQ','IR','IS','IT','JE','JM','JO','JP','KE','KG','KH','KI','KM','KN','KP','KR','KW','KY','KZ','LA','LB','LC','LI','LK','LR','LS','LT','LU','LV','LY','MA','MC','MD','ME','MF','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MX','MY','MZ','NA','NC','NE','NF','NG','NI','NL','NO','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PL','PM','PN','PR','PS','PT','PW','PY','QA','RE','RO','RS','RU','RW','SA','SB','SC','SD','SE','SG','SH','SI','SJ','SK','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ','TC','TD','TF','TG','TH','TJ','TK','TL','TM','TN','TO','TR','TT','TV','TW','TZ','UA','UG','UM','US','UY','UZ','VA','VC','VE','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZW']
// Precomputed [normalizedName, code] pairs in the display locale.
const COUNTRY_NAME_INDEX: [string, string][] = ALPHA2.map((code) => [
normalize(localizedCountry(code) ?? code),
code,
])
// Country codes whose localized name contains the search term.
const countryCodesMatching = (term: string): string[] => {
const t = normalize(term)
if (!t) return []
return COUNTRY_NAME_INDEX.filter(([name]) => name.includes(t)).map(([, code]) => code)
}
export default async function HomePage({
searchParams,
}: {
@ -41,6 +67,9 @@ export default async function HomePage({
const payload = await getPayload({ config: await config })
// Country is stored as an ISO code, so resolve any country *name* in the
// search term to its code(s) and match those too.
const countryCodes = countryCodesMatching(search)
const where: Where = search
? {
or: [
@ -48,6 +77,7 @@ export default async function HomePage({
{ email: { like: search } },
{ city: { like: search } },
{ country: { like: search } },
...(countryCodes.length ? [{ country: { in: countryCodes } }] : []),
],
}
: {}
@ -87,6 +117,8 @@ export default async function HomePage({
countryName: localizedCountry(p.country),
countryFlag: flag(p.country),
impactedPeople: p.impactedPeople ?? null,
churchRegistered: Boolean(p.churchRegistered),
registrationNumber: p.registrationNumber ?? null,
inTelegram: Boolean(p.inTelegram),
notes: p.notes ?? null,
letterUrl: letter?.url ?? null,

View File

@ -45,10 +45,21 @@ export const Pastors: CollectionConfig = {
type: 'number',
min: 0,
},
{
name: 'churchRegistered',
label: 'Church Registered',
type: 'checkbox',
defaultValue: false,
},
{
name: 'registrationNumber',
label: 'Registration Number',
label: 'Registration Code',
type: 'text',
admin: {
// Only relevant when the church is registered.
condition: (data) => Boolean(data?.churchRegistered),
description: 'Official registration code for the church.',
},
},
{
name: 'inTelegram',

View File

@ -232,6 +232,10 @@ export interface Pastor {
*/
country?: string | null;
impactedPeople?: number | null;
churchRegistered?: boolean | null;
/**
* Official registration code for the church.
*/
registrationNumber?: string | null;
inTelegram?: boolean | null;
/**
@ -434,6 +438,7 @@ export interface PastorsSelect<T extends boolean = true> {
state?: T;
country?: T;
impactedPeople?: T;
churchRegistered?: T;
registrationNumber?: T;
inTelegram?: T;
telephone?: T;