From 4bdfa69dbebcb7e10f94b741759ad85b99cc7212 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 22 Jan 2026 12:34:38 -0800 Subject: [PATCH] feat(api): Add custom_name field to user_access table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to set custom display names for their beneficiaries (e.g., "Mom", "Dad" instead of the real name). The custom_name is stored per-user in user_access, so different caregivers can have different names for the same beneficiary. Changes: - Migration 009: Add custom_name column to user_access - API: Return customName in GET /me/beneficiaries endpoints - API: New PATCH /me/beneficiaries/:id/custom-name endpoint - Types: Add customName to Beneficiary interface - api.ts: Add updateBeneficiaryCustomName method 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/migrations/009_add_custom_name.sql | 17 ++++++ backend/src/routes/beneficiaries.js | 69 +++++++++++++++++++++- services/api.ts | 35 +++++++++++ types/index.ts | 1 + 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/009_add_custom_name.sql diff --git a/backend/migrations/009_add_custom_name.sql b/backend/migrations/009_add_custom_name.sql new file mode 100644 index 0000000..cf376a7 --- /dev/null +++ b/backend/migrations/009_add_custom_name.sql @@ -0,0 +1,17 @@ +-- Migration 009: Add custom_name to user_access +-- Description: Allows users to give custom names to their beneficiaries +-- Date: 2025-01-22 + +-- Add custom_name column to user_access table +-- This allows each accessor to have their own custom name for a beneficiary +-- Example: "Mom", "Dad", "Grandma" instead of the beneficiary's real name +ALTER TABLE user_access +ADD COLUMN IF NOT EXISTS custom_name VARCHAR(100); + +-- Add comment for documentation +COMMENT ON COLUMN user_access.custom_name IS 'Custom display name set by the accessor for this beneficiary (e.g., "Mom", "Dad")'; + +-- Verify the change +SELECT column_name, data_type, character_maximum_length +FROM information_schema.columns +WHERE table_name = 'user_access' AND column_name = 'custom_name'; diff --git a/backend/src/routes/beneficiaries.js b/backend/src/routes/beneficiaries.js index f136dd9..8444ad2 100644 --- a/backend/src/routes/beneficiaries.js +++ b/backend/src/routes/beneficiaries.js @@ -190,7 +190,7 @@ router.get('/', async (req, res) => { // Get access records with beneficiary_id (points to beneficiaries table) const { data: accessRecords, error: accessError } = await supabase .from('user_access') - .select('id, beneficiary_id, role, granted_at') + .select('id, beneficiary_id, role, granted_at, custom_name') .eq('accessor_id', userId); if (accessError) { @@ -244,6 +244,7 @@ router.get('/', async (req, res) => { role: record.role, grantedAt: record.granted_at, name: beneficiary.name, + customName: record.custom_name || null, // User's custom name for this beneficiary phone: beneficiary.phone, address: beneficiary.address || null, avatarUrl: beneficiary.avatar_url, @@ -279,7 +280,7 @@ router.get('/:id', async (req, res) => { // Check user has access - beneficiaryId is now from beneficiaries table const { data: access, error: accessError } = await supabase .from('user_access') - .select('role, beneficiary_id') + .select('role, beneficiary_id, custom_name') .eq('accessor_id', userId) .eq('beneficiary_id', beneficiaryId) .single(); @@ -321,6 +322,7 @@ router.get('/:id', async (req, res) => { res.json({ id: beneficiary.id, name: beneficiary.name, + customName: access.custom_name || null, // User's custom name for this beneficiary phone: beneficiary.phone, address: beneficiary.address || null, avatarUrl: beneficiary.avatar_url, @@ -1172,4 +1174,67 @@ router.patch('/:id/equipment-status', authMiddleware, async (req, res) => { } }); +/** + * PATCH /api/me/beneficiaries/:id/custom-name + * Updates the user's custom name for a beneficiary + * This is stored in user_access (per-user, not global) + */ +router.patch('/:id/custom-name', async (req, res) => { + try { + const userId = req.user.userId; + const beneficiaryId = parseInt(req.params.id, 10); + const { customName } = req.body; + + console.log('[BENEFICIARY] Custom name update:', { userId, beneficiaryId, customName }); + + // Validate custom name (allow null to clear, or string up to 100 chars) + if (customName !== null && customName !== undefined) { + if (typeof customName !== 'string') { + return res.status(400).json({ error: 'customName must be a string or null' }); + } + if (customName.length > 100) { + return res.status(400).json({ error: 'customName must be 100 characters or less' }); + } + } + + // Check user has access to this beneficiary + const { data: access, error: accessError } = await supabase + .from('user_access') + .select('id, role') + .eq('accessor_id', userId) + .eq('beneficiary_id', beneficiaryId) + .single(); + + if (accessError || !access) { + return res.status(403).json({ error: 'Access denied to this beneficiary' }); + } + + // Update custom_name in user_access + const { data: updated, error: updateError } = await supabase + .from('user_access') + .update({ + custom_name: customName || null // Empty string becomes null + }) + .eq('id', access.id) + .select('id, custom_name') + .single(); + + if (updateError) { + console.error('[BENEFICIARY] Custom name update error:', updateError); + return res.status(500).json({ error: 'Failed to update custom name' }); + } + + console.log('[BENEFICIARY] Custom name updated:', { beneficiaryId, customName: updated.custom_name }); + + res.json({ + success: true, + customName: updated.custom_name + }); + + } catch (error) { + console.error('[BENEFICIARY] Custom name error:', error); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/services/api.ts b/services/api.ts index 3b932f2..e83de54 100644 --- a/services/api.ts +++ b/services/api.ts @@ -673,6 +673,7 @@ class ApiService { const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({ id: item.id, name: item.name || item.email, + customName: item.customName || null, // User's custom name for this beneficiary avatar: item.avatarUrl || undefined, // Use uploaded avatar from server status: 'offline' as const, email: item.email, @@ -725,6 +726,7 @@ class ApiService { const beneficiary: Beneficiary = { id: data.id, name: data.name || data.email, + customName: data.customName || null, // User's custom name for this beneficiary avatar: data.avatarUrl || undefined, status: 'offline' as const, email: data.email, @@ -943,6 +945,39 @@ class ApiService { } } + // Update beneficiary custom name (per-user, stored in user_access) + async updateBeneficiaryCustomName( + id: number, + customName: string | null + ): Promise> { + const token = await this.getToken(); + + if (!token) { + return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; + } + + try { + const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/custom-name`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ customName }), + }); + + const data = await response.json(); + + if (!response.ok) { + return { ok: false, error: { message: data.error || 'Failed to update custom name' } }; + } + + return { data: { customName: data.customName }, ok: true }; + } catch (error) { + return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; + } + } + // AI Chat - deploymentId is required, no default value for security (LEGACY API) async sendMessage(question: string, deploymentId: string): Promise> { if (!deploymentId) { diff --git a/types/index.ts b/types/index.ts index c2190b5..98f8e9c 100644 --- a/types/index.ts +++ b/types/index.ts @@ -83,6 +83,7 @@ export interface Deployment { export interface Beneficiary { id: number; name: string; + customName?: string | null; // User's custom display name (e.g., "Mom", "Dad") avatar?: string; device_id?: string; status: 'online' | 'offline';