db-ministerial/src/app/(frontend)/PastorsTable.tsx

383 lines
13 KiB
TypeScript

'use client'
import React, { useRef, useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { createPastor, updatePastor, uploadLetter } from './actions'
export type PastorRow = {
id: number
name: string
email: string | null
telephone: string[]
city: string | null
state: string | null
country: string | null
countryName: string | null
countryFlag: string
inTelegram: boolean
notes: string | null
letterUrl: string | null
letterName: string | null
}
export function PastorsTable({ rows }: { rows: PastorRow[] }) {
const [editing, setEditing] = useState<PastorRow | null>(null)
const [uploading, setUploading] = useState<PastorRow | null>(null)
const [creating, setCreating] = useState(false)
return (
<>
<div className="flex justify-end mb-4">
<button type="button" className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>
+ Crear pastor
</button>
</div>
{rows.length === 0 ? (
<div className="card bg-base-100 shadow-sm">
<div className="card-body items-center text-center">
<h3 className="card-title">No se encontraron pastores</h3>
<p className="text-base-content/60">Intenta con otro término de búsqueda.</p>
</div>
</div>
) : (
<div className="overflow-x-auto rounded-box border border-base-300 bg-base-100">
<table className="table table-zebra table-pin-rows">
<thead>
<tr>
<th>Nombre</th>
<th>Correo</th>
<th>Teléfonos</th>
<th>Ciudad</th>
<th>País</th>
<th className="text-center">Telegram</th>
<th>Carta</th>
<th className="text-right">Acciones</th>
</tr>
</thead>
<tbody>
{rows.map((p) => (
<tr key={p.id}>
<td className="font-medium whitespace-nowrap">{p.name}</td>
<td>
{p.email ? (
<a href={`mailto:${p.email}`} className="link link-hover">
{p.email}
</a>
) : (
<span className="opacity-40"></span>
)}
</td>
<td>
{p.telephone.length ? (
<div className="flex flex-col gap-0.5">
{p.telephone.map((t, i) => (
<a key={i} href={`tel:${t}`} className="link link-hover whitespace-nowrap">
{t}
</a>
))}
</div>
) : (
<span className="opacity-40"></span>
)}
</td>
<td className="whitespace-nowrap">{p.city || <span className="opacity-40"></span>}</td>
<td className="whitespace-nowrap">
{p.countryName ? (
<span>
{p.countryFlag} {p.countryName}
</span>
) : (
<span className="opacity-40"></span>
)}
</td>
<td className="text-center">
{p.inTelegram ? (
<span className="badge badge-success badge-sm"></span>
) : (
<span className="badge badge-ghost badge-sm">No</span>
)}
</td>
<td>
{p.letterUrl ? (
<a
href={p.letterUrl}
target="_blank"
rel="noopener noreferrer"
download={p.letterName ?? undefined}
className="btn btn-xs btn-outline btn-success"
>
Descargar
</a>
) : (
<button
type="button"
className="btn btn-xs btn-outline"
onClick={() => setUploading(p)}
>
Subir
</button>
)}
</td>
<td className="text-right">
<button
type="button"
className="btn btn-xs btn-ghost"
onClick={() => setEditing(p)}
>
Editar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{creating && <PastorModal key="create" pastor={null} onClose={() => setCreating(false)} />}
{editing && (
<PastorModal key={`edit-${editing.id}`} pastor={editing} onClose={() => setEditing(null)} />
)}
{uploading && (
<UploadModal
key={`upload-${uploading.id}`}
pastor={uploading}
onClose={() => setUploading(null)}
/>
)}
</>
)
}
function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: () => void }) {
const router = useRouter()
const isCreate = pastor === null
const [pending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const [name, setName] = useState(pastor?.name ?? '')
const [email, setEmail] = useState(pastor?.email ?? '')
const [phones, setPhones] = useState<string[]>(
pastor?.telephone.length ? pastor.telephone : [''],
)
const [city, setCity] = useState(pastor?.city ?? '')
const [state, setState] = useState(pastor?.state ?? '')
const [country, setCountry] = useState(pastor?.country ?? '')
const [inTelegram, setInTelegram] = useState(pastor?.inTelegram ?? false)
const [notes, setNotes] = useState(pastor?.notes ?? '')
const setPhone = (i: number, v: string) =>
setPhones((prev) => prev.map((p, idx) => (idx === i ? v : p)))
const addPhone = () => setPhones((prev) => [...prev, ''])
const removePhone = (i: number) => setPhones((prev) => prev.filter((_, idx) => idx !== i))
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
setError(null)
startTransition(async () => {
const input = { name, email, telephone: phones, city, state, country, inTelegram, notes }
const res = isCreate ? await createPastor(input) : await updatePastor(pastor.id, input)
if (res?.ok) {
router.refresh()
onClose()
} else {
setError(
('error' in res && res.error) ||
(isCreate ? 'No se pudo crear el pastor.' : 'No se pudieron guardar los cambios.'),
)
}
})
}
return (
<dialog className="modal modal-open">
<div className="modal-box max-w-xl">
<h3 className="text-lg font-bold pb-3 mb-5 border-b border-base-300">
{isCreate ? 'Crear pastor' : 'Editar pastor'}
</h3>
<form
onSubmit={onSubmit}
className="grid grid-cols-1 sm:grid-cols-[7.5rem_1fr] sm:items-center gap-x-4 gap-y-4"
>
<label htmlFor="f-name" className="text-sm font-medium sm:text-right sm:pr-1">
Nombre
</label>
<input
id="f-name"
className="input input-bordered w-full"
value={name}
placeholder="Nombre completo"
onChange={(e) => setName(e.target.value)}
required
/>
<label htmlFor="f-email" className="text-sm font-medium sm:text-right sm:pr-1">
Correo
</label>
<input
id="f-email"
type="email"
className="input input-bordered w-full"
value={email}
placeholder="correo@ejemplo.com"
onChange={(e) => setEmail(e.target.value)}
/>
<span className="text-sm font-medium sm:text-right sm:pr-1 sm:self-start sm:pt-3">
Teléfonos
</span>
<div className="flex flex-col gap-2">
{phones.map((p, i) => (
<div key={i} className="join w-full">
<input
className="input input-bordered join-item w-full"
value={p}
placeholder="+1 555 123 4567"
onChange={(e) => setPhone(i, e.target.value)}
/>
<button
type="button"
className="btn btn-outline join-item"
onClick={() => removePhone(i)}
aria-label="Eliminar número"
>
</button>
</div>
))}
<button type="button" className="btn btn-sm btn-ghost self-start" onClick={addPhone}>
+ Agregar número
</button>
</div>
<label htmlFor="f-city" className="text-sm font-medium sm:text-right sm:pr-1">
Ciudad
</label>
<input
id="f-city"
className="input input-bordered w-full"
value={city}
onChange={(e) => setCity(e.target.value)}
/>
<label htmlFor="f-state" className="text-sm font-medium sm:text-right sm:pr-1">
Estado
</label>
<input
id="f-state"
className="input input-bordered w-full"
value={state}
onChange={(e) => setState(e.target.value)}
/>
<label htmlFor="f-country" className="text-sm font-medium sm:text-right sm:pr-1">
País
</label>
<div className="flex items-center gap-2">
<input
id="f-country"
className="input input-bordered w-24 uppercase"
value={country}
maxLength={2}
placeholder="MX"
onChange={(e) => setCountry(e.target.value)}
/>
<span className="text-xs text-base-content/50">Código ISO de 2 letras</span>
</div>
<span className="text-sm font-medium sm:text-right sm:pr-1">En Telegram</span>
<input
type="checkbox"
className="toggle toggle-success"
checked={inTelegram}
onChange={(e) => setInTelegram(e.target.checked)}
/>
<label htmlFor="f-notes" className="text-sm font-medium sm:text-right sm:pr-1 sm:self-start sm:pt-3">
Notas
</label>
<textarea
id="f-notes"
className="textarea textarea-bordered w-full"
rows={2}
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
{error && (
<div role="alert" className="alert alert-error alert-soft sm:col-span-2 py-2">
<span className="text-sm">{error}</span>
</div>
)}
<div className="modal-action sm:col-span-2 mt-2">
<button type="button" className="btn btn-ghost" onClick={onClose} disabled={pending}>
Cancelar
</button>
<button type="submit" className="btn btn-primary" disabled={pending}>
{pending && <span className="loading loading-spinner loading-sm" />}
{isCreate ? 'Crear' : 'Guardar'}
</button>
</div>
</form>
</div>
<button type="button" className="modal-backdrop" onClick={onClose} aria-label="Cerrar" />
</dialog>
)
}
function UploadModal({ pastor, onClose }: { pastor: PastorRow; onClose: () => void }) {
const router = useRouter()
const formRef = useRef<HTMLFormElement>(null)
const [pending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
setError(null)
const formData = new FormData(formRef.current!)
startTransition(async () => {
const res = await uploadLetter(pastor.id, formData)
if (res?.ok) {
router.refresh()
onClose()
} else {
setError(res?.error ?? 'Error al subir el archivo.')
}
})
}
return (
<dialog className="modal modal-open">
<div className="modal-box">
<h3 className="text-lg font-bold">Subir carta</h3>
<p className="text-sm text-base-content/60 mt-1 mb-4">
Adjunta una carta para <span className="font-medium">{pastor.name}</span>. Solo PDF o Word.
</p>
<form ref={formRef} onSubmit={onSubmit} className="flex flex-col gap-4">
<input
type="file"
name="file"
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
className="file-input file-input-bordered w-full"
required
/>
{error && <p className="text-error text-sm">{error}</p>}
<div className="modal-action">
<button type="button" className="btn btn-ghost" onClick={onClose} disabled={pending}>
Cancelar
</button>
<button type="submit" className="btn btn-primary" disabled={pending}>
{pending && <span className="loading loading-spinner loading-sm" />}
Subir
</button>
</div>
</form>
</div>
<button type="button" className="modal-backdrop" onClick={onClose} aria-label="Cerrar" />
</dialog>
)
}