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:
parent
efeee14752
commit
ba08fc087b
|
|
@ -18,6 +18,8 @@ export type PastorRow = {
|
||||||
countryName: string | null
|
countryName: string | null
|
||||||
countryFlag: string
|
countryFlag: string
|
||||||
impactedPeople: number | null
|
impactedPeople: number | null
|
||||||
|
churchRegistered: boolean
|
||||||
|
registrationNumber: string | null
|
||||||
inTelegram: boolean
|
inTelegram: boolean
|
||||||
notes: string | null
|
notes: string | null
|
||||||
letterUrl: string | null
|
letterUrl: string | null
|
||||||
|
|
@ -36,6 +38,8 @@ type ColumnKey =
|
||||||
| 'state'
|
| 'state'
|
||||||
| 'country'
|
| 'country'
|
||||||
| 'impactedPeople'
|
| 'impactedPeople'
|
||||||
|
| 'churchRegistered'
|
||||||
|
| 'registrationNumber'
|
||||||
| 'inTelegram'
|
| 'inTelegram'
|
||||||
| 'letter'
|
| 'letter'
|
||||||
|
|
||||||
|
|
@ -122,6 +126,22 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
||||||
cell: (p) =>
|
cell: (p) =>
|
||||||
p.impactedPeople === null ? dash : p.impactedPeople.toLocaleString('es'),
|
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">Sí</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',
|
key: 'inTelegram',
|
||||||
label: 'Telegram',
|
label: 'Telegram',
|
||||||
|
|
@ -343,6 +363,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
||||||
const [impactedPeople, setImpactedPeople] = useState(
|
const [impactedPeople, setImpactedPeople] = useState(
|
||||||
pastor?.impactedPeople != null ? String(pastor.impactedPeople) : '',
|
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 [inTelegram, setInTelegram] = useState(pastor?.inTelegram ?? false)
|
||||||
const [notes, setNotes] = useState(pastor?.notes ?? '')
|
const [notes, setNotes] = useState(pastor?.notes ?? '')
|
||||||
|
|
||||||
|
|
@ -365,6 +387,8 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
||||||
state,
|
state,
|
||||||
country,
|
country,
|
||||||
impactedPeople: impactedPeople.trim() === '' ? null : Number(impactedPeople),
|
impactedPeople: impactedPeople.trim() === '' ? null : Number(impactedPeople),
|
||||||
|
churchRegistered,
|
||||||
|
registrationNumber: churchRegistered ? registrationNumber : null,
|
||||||
inTelegram,
|
inTelegram,
|
||||||
notes,
|
notes,
|
||||||
}
|
}
|
||||||
|
|
@ -510,6 +534,32 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
||||||
onChange={(e) => setImpactedPeople(e.target.value)}
|
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>
|
<span className="text-sm font-medium sm:text-right sm:pr-1">En Telegram</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ export type PastorUpdateInput = {
|
||||||
state?: string | null
|
state?: string | null
|
||||||
country?: string | null
|
country?: string | null
|
||||||
impactedPeople?: number | null
|
impactedPeople?: number | null
|
||||||
|
churchRegistered?: boolean
|
||||||
|
registrationNumber?: string | null
|
||||||
inTelegram?: boolean
|
inTelegram?: boolean
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +44,9 @@ export async function createPastor(data: PastorUpdateInput) {
|
||||||
data.impactedPeople === null || data.impactedPeople === undefined
|
data.impactedPeople === null || data.impactedPeople === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: Number(data.impactedPeople),
|
: 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),
|
inTelegram: Boolean(data.inTelegram),
|
||||||
notes: data.notes || undefined,
|
notes: data.notes || undefined,
|
||||||
},
|
},
|
||||||
|
|
@ -71,6 +76,9 @@ export async function updatePastor(id: number, data: PastorUpdateInput) {
|
||||||
data.impactedPeople === null || data.impactedPeople === undefined
|
data.impactedPeople === null || data.impactedPeople === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: Number(data.impactedPeople),
|
: 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),
|
inTelegram: Boolean(data.inTelegram),
|
||||||
notes: data.notes || undefined,
|
notes: data.notes || undefined,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,32 @@ const flag = (code?: string | null): string => {
|
||||||
return String.fromCodePoint(base + (cc.charCodeAt(0) - 65), base + (cc.charCodeAt(1) - 65))
|
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({
|
export default async function HomePage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -41,6 +67,9 @@ export default async function HomePage({
|
||||||
|
|
||||||
const payload = await getPayload({ config: await config })
|
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
|
const where: Where = search
|
||||||
? {
|
? {
|
||||||
or: [
|
or: [
|
||||||
|
|
@ -48,6 +77,7 @@ export default async function HomePage({
|
||||||
{ email: { like: search } },
|
{ email: { like: search } },
|
||||||
{ city: { like: search } },
|
{ city: { like: search } },
|
||||||
{ country: { like: search } },
|
{ country: { like: search } },
|
||||||
|
...(countryCodes.length ? [{ country: { in: countryCodes } }] : []),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
@ -87,6 +117,8 @@ export default async function HomePage({
|
||||||
countryName: localizedCountry(p.country),
|
countryName: localizedCountry(p.country),
|
||||||
countryFlag: flag(p.country),
|
countryFlag: flag(p.country),
|
||||||
impactedPeople: p.impactedPeople ?? null,
|
impactedPeople: p.impactedPeople ?? null,
|
||||||
|
churchRegistered: Boolean(p.churchRegistered),
|
||||||
|
registrationNumber: p.registrationNumber ?? null,
|
||||||
inTelegram: Boolean(p.inTelegram),
|
inTelegram: Boolean(p.inTelegram),
|
||||||
notes: p.notes ?? null,
|
notes: p.notes ?? null,
|
||||||
letterUrl: letter?.url ?? null,
|
letterUrl: letter?.url ?? null,
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,21 @@ export const Pastors: CollectionConfig = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'churchRegistered',
|
||||||
|
label: 'Church Registered',
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'registrationNumber',
|
name: 'registrationNumber',
|
||||||
label: 'Registration Number',
|
label: 'Registration Code',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
// Only relevant when the church is registered.
|
||||||
|
condition: (data) => Boolean(data?.churchRegistered),
|
||||||
|
description: 'Official registration code for the church.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'inTelegram',
|
name: 'inTelegram',
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,10 @@ export interface Pastor {
|
||||||
*/
|
*/
|
||||||
country?: string | null;
|
country?: string | null;
|
||||||
impactedPeople?: number | null;
|
impactedPeople?: number | null;
|
||||||
|
churchRegistered?: boolean | null;
|
||||||
|
/**
|
||||||
|
* Official registration code for the church.
|
||||||
|
*/
|
||||||
registrationNumber?: string | null;
|
registrationNumber?: string | null;
|
||||||
inTelegram?: boolean | null;
|
inTelegram?: boolean | null;
|
||||||
/**
|
/**
|
||||||
|
|
@ -434,6 +438,7 @@ export interface PastorsSelect<T extends boolean = true> {
|
||||||
state?: T;
|
state?: T;
|
||||||
country?: T;
|
country?: T;
|
||||||
impactedPeople?: T;
|
impactedPeople?: T;
|
||||||
|
churchRegistered?: T;
|
||||||
registrationNumber?: T;
|
registrationNumber?: T;
|
||||||
inTelegram?: T;
|
inTelegram?: T;
|
||||||
telephone?: T;
|
telephone?: T;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue