feat(api): Add custom_name field to user_access table
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 <noreply@anthropic.com>
This commit is contained in:
parent
f94121b848
commit
4bdfa69dbe
17
backend/migrations/009_add_custom_name.sql
Normal file
17
backend/migrations/009_add_custom_name.sql
Normal file
@ -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';
|
||||||
@ -190,7 +190,7 @@ router.get('/', async (req, res) => {
|
|||||||
// Get access records with beneficiary_id (points to beneficiaries table)
|
// Get access records with beneficiary_id (points to beneficiaries table)
|
||||||
const { data: accessRecords, error: accessError } = await supabase
|
const { data: accessRecords, error: accessError } = await supabase
|
||||||
.from('user_access')
|
.from('user_access')
|
||||||
.select('id, beneficiary_id, role, granted_at')
|
.select('id, beneficiary_id, role, granted_at, custom_name')
|
||||||
.eq('accessor_id', userId);
|
.eq('accessor_id', userId);
|
||||||
|
|
||||||
if (accessError) {
|
if (accessError) {
|
||||||
@ -244,6 +244,7 @@ router.get('/', async (req, res) => {
|
|||||||
role: record.role,
|
role: record.role,
|
||||||
grantedAt: record.granted_at,
|
grantedAt: record.granted_at,
|
||||||
name: beneficiary.name,
|
name: beneficiary.name,
|
||||||
|
customName: record.custom_name || null, // User's custom name for this beneficiary
|
||||||
phone: beneficiary.phone,
|
phone: beneficiary.phone,
|
||||||
address: beneficiary.address || null,
|
address: beneficiary.address || null,
|
||||||
avatarUrl: beneficiary.avatar_url,
|
avatarUrl: beneficiary.avatar_url,
|
||||||
@ -279,7 +280,7 @@ router.get('/:id', async (req, res) => {
|
|||||||
// Check user has access - beneficiaryId is now from beneficiaries table
|
// Check user has access - beneficiaryId is now from beneficiaries table
|
||||||
const { data: access, error: accessError } = await supabase
|
const { data: access, error: accessError } = await supabase
|
||||||
.from('user_access')
|
.from('user_access')
|
||||||
.select('role, beneficiary_id')
|
.select('role, beneficiary_id, custom_name')
|
||||||
.eq('accessor_id', userId)
|
.eq('accessor_id', userId)
|
||||||
.eq('beneficiary_id', beneficiaryId)
|
.eq('beneficiary_id', beneficiaryId)
|
||||||
.single();
|
.single();
|
||||||
@ -321,6 +322,7 @@ router.get('/:id', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
id: beneficiary.id,
|
id: beneficiary.id,
|
||||||
name: beneficiary.name,
|
name: beneficiary.name,
|
||||||
|
customName: access.custom_name || null, // User's custom name for this beneficiary
|
||||||
phone: beneficiary.phone,
|
phone: beneficiary.phone,
|
||||||
address: beneficiary.address || null,
|
address: beneficiary.address || null,
|
||||||
avatarUrl: beneficiary.avatar_url,
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@ -673,6 +673,7 @@ class ApiService {
|
|||||||
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
|
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name || item.email,
|
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
|
avatar: item.avatarUrl || undefined, // Use uploaded avatar from server
|
||||||
status: 'offline' as const,
|
status: 'offline' as const,
|
||||||
email: item.email,
|
email: item.email,
|
||||||
@ -725,6 +726,7 @@ class ApiService {
|
|||||||
const beneficiary: Beneficiary = {
|
const beneficiary: Beneficiary = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name || data.email,
|
name: data.name || data.email,
|
||||||
|
customName: data.customName || null, // User's custom name for this beneficiary
|
||||||
avatar: data.avatarUrl || undefined,
|
avatar: data.avatarUrl || undefined,
|
||||||
status: 'offline' as const,
|
status: 'offline' as const,
|
||||||
email: data.email,
|
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<ApiResponse<{ customName: string | null }>> {
|
||||||
|
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)
|
// AI Chat - deploymentId is required, no default value for security (LEGACY API)
|
||||||
async sendMessage(question: string, deploymentId: string): Promise<ApiResponse<ChatResponse>> {
|
async sendMessage(question: string, deploymentId: string): Promise<ApiResponse<ChatResponse>> {
|
||||||
if (!deploymentId) {
|
if (!deploymentId) {
|
||||||
|
|||||||
@ -83,6 +83,7 @@ export interface Deployment {
|
|||||||
export interface Beneficiary {
|
export interface Beneficiary {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
customName?: string | null; // User's custom display name (e.g., "Mom", "Dad")
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
status: 'online' | 'offline';
|
status: 'online' | 'offline';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user