feat(settings): add Pediatrician Name field alongside phone
- New families.pediatrician_name column (migration 0008) - Settings card: Name input above Phone input, single Save - Emergency page: shows doctor's name above the call button - AI medical redirects: personalised "Call Dr. X: +91…" message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
260187b0de
commit
90c8d13814
8 changed files with 54 additions and 23 deletions
2
drizzle/0008_pediatrician_name.sql
Normal file
2
drizzle/0008_pediatrician_name.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE families
|
||||||
|
ADD COLUMN IF NOT EXISTS pediatrician_name text;
|
||||||
|
|
@ -57,6 +57,13 @@
|
||||||
"when": 1748480400000,
|
"when": 1748480400000,
|
||||||
"tag": "0007_subscription_status",
|
"tag": "0007_subscription_status",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1748566800000,
|
||||||
|
"tag": "0008_pediatrician_name",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -17,10 +17,12 @@ const RULE_META: Record<string, { icon: string; title: string; color: string }>
|
||||||
|
|
||||||
export default function EmergencyPage() {
|
export default function EmergencyPage() {
|
||||||
const [phone, setPhone] = useState<string | null>(null);
|
const [phone, setPhone] = useState<string | null>(null);
|
||||||
|
const [name, setName] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/family").then(r => r.json()).then(d => {
|
fetch("/api/family").then(r => r.json()).then(d => {
|
||||||
setPhone(d.family?.pediatrician_phone || null);
|
setPhone(d.family?.pediatrician_phone || null);
|
||||||
|
setName(d.family?.pediatrician_name || null);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -38,20 +40,25 @@ export default function EmergencyPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Call button */}
|
{/* Call button */}
|
||||||
<div className="p-4">
|
<div className="p-4 space-y-1">
|
||||||
{phone ? (
|
{phone ? (
|
||||||
|
<>
|
||||||
|
{name && (
|
||||||
|
<p className="text-center text-sm font-medium text-gray-600 dark:text-gray-300 mb-2">{name}</p>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={`tel:${phone}`}
|
href={`tel:${phone}`}
|
||||||
className="w-full flex items-center justify-center gap-3 bg-red-500 hover:bg-red-600 text-white rounded-2xl p-4 text-lg font-bold shadow-md shadow-red-200 dark:shadow-red-900/30 transition-colors"
|
className="w-full flex items-center justify-center gap-3 bg-red-500 hover:bg-red-600 text-white rounded-2xl p-4 text-lg font-bold shadow-md shadow-red-200 dark:shadow-red-900/30 transition-colors"
|
||||||
>
|
>
|
||||||
📞 Call Pediatrician Now
|
📞 Call Pediatrician Now
|
||||||
</a>
|
</a>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href="/settings"
|
href="/settings"
|
||||||
className="w-full flex items-center justify-center gap-2 bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-2xl p-4 font-medium"
|
className="w-full flex items-center justify-center gap-2 bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-2xl p-4 font-medium"
|
||||||
>
|
>
|
||||||
+ Add pediatrician phone in Settings
|
+ Add pediatrician info in Settings
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export default function SettingsPage() {
|
||||||
const [inviteRole, setInviteRole] = useState("caregiver");
|
const [inviteRole, setInviteRole] = useState("caregiver");
|
||||||
const [inviteLoading, setInviteLoading] = useState(false);
|
const [inviteLoading, setInviteLoading] = useState(false);
|
||||||
const [pedPhone, setPedPhone] = useState("");
|
const [pedPhone, setPedPhone] = useState("");
|
||||||
|
const [pedName, setPedName] = useState("");
|
||||||
const [pedSaving, setPedSaving] = useState(false);
|
const [pedSaving, setPedSaving] = useState(false);
|
||||||
|
|
||||||
// Check if can invite more members (client-side pre-check; server enforces)
|
// Check if can invite more members (client-side pre-check; server enforces)
|
||||||
|
|
@ -60,6 +61,7 @@ export default function SettingsPage() {
|
||||||
fetchInvites();
|
fetchInvites();
|
||||||
fetch("/api/family").then(r => r.json()).then(d => {
|
fetch("/api/family").then(r => r.json()).then(d => {
|
||||||
if (d.family?.pediatrician_phone) setPedPhone(d.family.pediatrician_phone);
|
if (d.family?.pediatrician_phone) setPedPhone(d.family.pediatrician_phone);
|
||||||
|
if (d.family?.pediatrician_name) setPedName(d.family.pediatrician_name);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}, [familyId]);
|
}, [familyId]);
|
||||||
|
|
@ -92,12 +94,12 @@ export default function SettingsPage() {
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const savePedPhone = async () => {
|
const savePedInfo = async () => {
|
||||||
setPedSaving(true);
|
setPedSaving(true);
|
||||||
await fetch("/api/family", {
|
await fetch("/api/family", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ pediatricianPhone: pedPhone }),
|
body: JSON.stringify({ pediatricianPhone: pedPhone, pediatricianName: pedName }),
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
setPedSaving(false);
|
setPedSaving(false);
|
||||||
};
|
};
|
||||||
|
|
@ -356,16 +358,22 @@ export default function SettingsPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pediatrician Phone */}
|
{/* Pediatrician */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 space-y-2">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xl">🏥</span>
|
<span className="text-xl">🏥</span>
|
||||||
<div className="font-medium dark:text-white">Pediatrician Phone</div>
|
<div className="font-medium dark:text-white">Pediatrician</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">Shown on the emergency guide and in AI medical redirects.</p>
|
<p className="text-xs text-gray-400 dark:text-gray-500">Shown on the emergency guide and in AI medical redirects.</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={pedName}
|
||||||
|
onChange={e => setPedName(e.target.value)}
|
||||||
|
placeholder="Dr. Priya Sharma"
|
||||||
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input type="tel" value={pedPhone} onChange={e => setPedPhone(e.target.value)} placeholder="+91 98765 43210" className="flex-1" />
|
<Input type="tel" value={pedPhone} onChange={e => setPedPhone(e.target.value)} placeholder="+91 98765 43210" className="flex-1" />
|
||||||
<Button size="sm" loading={pedSaving} onClick={savePedPhone}>Save</Button>
|
<Button size="sm" loading={pedSaving} onClick={savePedInfo}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/medical/emergency" className="text-xs text-rose-500 dark:text-rose-400">
|
<Link href="/medical/emergency" className="text-xs text-rose-500 dark:text-rose-400">
|
||||||
View Emergency Guide →
|
View Emergency Guide →
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,10 @@ export async function POST(request: Request) {
|
||||||
// ── 1. HARD GUARDRAIL: keyword-based medical detection (most conservative) ──
|
// ── 1. HARD GUARDRAIL: keyword-based medical detection (most conservative) ──
|
||||||
const medicalIntent = detectMedicalIntent(lastUserMsg);
|
const medicalIntent = detectMedicalIntent(lastUserMsg);
|
||||||
if (medicalIntent.isMedical) {
|
if (medicalIntent.isMedical) {
|
||||||
const families = await sql`SELECT pediatrician_phone FROM families WHERE id = ${familyId} LIMIT 1`;
|
const families = await sql`SELECT pediatrician_phone, pediatrician_name FROM families WHERE id = ${familyId} LIMIT 1`;
|
||||||
const phone = families[0]?.pediatrician_phone;
|
const phone = families[0]?.pediatrician_phone;
|
||||||
|
const pedName = families[0]?.pediatrician_name;
|
||||||
|
const pedLabel = pedName ? `Dr. ${pedName.replace(/^Dr\.?\s*/i, "")}` : "your pediatrician";
|
||||||
|
|
||||||
const reply = [
|
const reply = [
|
||||||
`I can't interpret symptoms — that's a pediatrician's job, not mine.`,
|
`I can't interpret symptoms — that's a pediatrician's job, not mine.`,
|
||||||
|
|
@ -47,8 +49,8 @@ export async function POST(request: Request) {
|
||||||
ESCALATION_RULES[medicalIntent.category],
|
ESCALATION_RULES[medicalIntent.category],
|
||||||
``,
|
``,
|
||||||
phone
|
phone
|
||||||
? `Call your pediatrician now: ${phone}`
|
? `Call ${pedLabel} now: ${phone}`
|
||||||
: `Add your pediatrician's phone in Settings so I can show it here.`,
|
: `Add your pediatrician's info in Settings so I can show it here.`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
await logAudit({
|
await logAudit({
|
||||||
|
|
@ -84,13 +86,15 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
// Medical_redirect from classifier → also redirect
|
// Medical_redirect from classifier → also redirect
|
||||||
if (classification.intent === "medical_redirect") {
|
if (classification.intent === "medical_redirect") {
|
||||||
const families = await sql`SELECT pediatrician_phone FROM families WHERE id = ${familyId} LIMIT 1`;
|
const families = await sql`SELECT pediatrician_phone, pediatrician_name FROM families WHERE id = ${familyId} LIMIT 1`;
|
||||||
const phone = families[0]?.pediatrician_phone;
|
const phone = families[0]?.pediatrician_phone;
|
||||||
|
const pedName = families[0]?.pediatrician_name;
|
||||||
|
const pedLabel = pedName ? `Dr. ${pedName.replace(/^Dr\.?\s*/i, "")}` : "your pediatrician";
|
||||||
const reply = [
|
const reply = [
|
||||||
`That sounds like something your pediatrician should assess.`,
|
`That sounds like something your pediatrician should assess.`,
|
||||||
``,
|
``,
|
||||||
ESCALATION_RULES["default"],
|
ESCALATION_RULES["default"],
|
||||||
phone ? `Call: ${phone}` : `Add your pediatrician's phone in Settings.`,
|
phone ? `Call ${pedLabel}: ${phone}` : `Add your pediatrician's info in Settings.`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
await logUsage({ familyId, userId: session.userId, intent: "medical_redirect", durationMs: Date.now() - start });
|
await logUsage({ familyId, userId: session.userId, intent: "medical_redirect", durationMs: Date.now() - start });
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ export async function POST(req: Request) {
|
||||||
`ALTER TABLE family_invites ADD COLUMN IF NOT EXISTS accepted_at timestamp`,
|
`ALTER TABLE family_invites ADD COLUMN IF NOT EXISTS accepted_at timestamp`,
|
||||||
// subscription_status on families (0007) — payment-provider abstraction
|
// subscription_status on families (0007) — payment-provider abstraction
|
||||||
`ALTER TABLE families ADD COLUMN IF NOT EXISTS subscription_status varchar(20) DEFAULT NULL`,
|
`ALTER TABLE families ADD COLUMN IF NOT EXISTS subscription_status varchar(20) DEFAULT NULL`,
|
||||||
|
// pediatrician_name on families (0008)
|
||||||
|
`ALTER TABLE families ADD COLUMN IF NOT EXISTS pediatrician_name text`,
|
||||||
// circles tables (0003)
|
// circles tables (0003)
|
||||||
`CREATE TABLE IF NOT EXISTS circles (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_by uuid NOT NULL REFERENCES families(id), created_at timestamptz NOT NULL DEFAULT now())`,
|
`CREATE TABLE IF NOT EXISTS circles (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_by uuid NOT NULL REFERENCES families(id), created_at timestamptz NOT NULL DEFAULT now())`,
|
||||||
`CREATE TABLE IF NOT EXISTS circle_members (circle_id uuid NOT NULL REFERENCES circles(id) ON DELETE CASCADE, family_id uuid NOT NULL REFERENCES families(id), role text NOT NULL DEFAULT 'member', joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (circle_id, family_id))`,
|
`CREATE TABLE IF NOT EXISTS circle_members (circle_id uuid NOT NULL REFERENCES circles(id) ON DELETE CASCADE, family_id uuid NOT NULL REFERENCES families(id), role text NOT NULL DEFAULT 'member', joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (circle_id, family_id))`,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const family = await sql.unsafe(
|
const family = await sql.unsafe(
|
||||||
`SELECT id, name, tier, max_children, max_members FROM families WHERE id = $1`,
|
`SELECT id, name, tier, max_children, max_members, pediatrician_phone, pediatrician_name FROM families WHERE id = $1`,
|
||||||
[auth.session!.familyId]
|
[auth.session!.familyId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -31,11 +31,11 @@ export async function PATCH(request: Request) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, pediatricianPhone, tier } = body;
|
const { name, pediatricianPhone, pediatricianName, tier } = body;
|
||||||
|
|
||||||
await sql.unsafe(
|
await sql.unsafe(
|
||||||
`UPDATE families SET name = COALESCE($1, name), pediatrician_phone = COALESCE($2, pediatrician_phone), tier = COALESCE($3, tier), updated_at = NOW() WHERE id = $4`,
|
`UPDATE families SET name = COALESCE($1, name), pediatrician_phone = COALESCE($2, pediatrician_phone), pediatrician_name = COALESCE($3, pediatrician_name), tier = COALESCE($4, tier), updated_at = NOW() WHERE id = $5`,
|
||||||
[name, pediatricianPhone, tier, auth.session!.familyId]
|
[name, pediatricianPhone, pediatricianName, tier, auth.session!.familyId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export const families = pgTable("families", {
|
||||||
maxChildren: integer("max_children").default(1),
|
maxChildren: integer("max_children").default(1),
|
||||||
maxMembers: integer("max_members").default(2),
|
maxMembers: integer("max_members").default(2),
|
||||||
pediatricianPhone: text("pediatrician_phone"),
|
pediatricianPhone: text("pediatrician_phone"),
|
||||||
|
pediatricianName: text("pediatrician_name"),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue