Skip to content

Commit c1101a5

Browse files
fix: shopping price crash + add user role (parent/enfant) + auto-join family on register
1 parent a84728e commit c1101a5

8 files changed

Lines changed: 64 additions & 15 deletions

File tree

client/src/contexts/AuthContext.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ interface User {
66
email: string;
77
name: string;
88
is_owner?: boolean;
9+
role?: string;
910
}
1011

1112
interface AuthContextType {
1213
user: User | null;
1314
loading: boolean;
1415
login: (email: string, password: string) => Promise<void>;
15-
register: (email: string, password: string, name: string, inviteToken?: string) => Promise<void>;
16+
register: (email: string, password: string, name: string, inviteToken?: string, role?: string) => Promise<void>;
1617
joinFamily: (inviteToken: string) => Promise<void>;
1718
leaveFamily: () => Promise<void>;
1819
logout: () => void;
@@ -91,8 +92,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
9192
}
9293
};
9394

94-
const register = async (email: string, password: string, name: string, inviteToken?: string) => {
95-
const response = await api.register(email, password, name, inviteToken);
95+
const register = async (email: string, password: string, name: string, inviteToken?: string, role?: string) => {
96+
const response = await api.register(email, password, name, inviteToken, role);
9697
if (response.success && response.user) {
9798
setUser(response.user);
9899
localStorage.setItem('user', JSON.stringify(response.user));

client/src/lib/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ class ApiClient {
100100
return response;
101101
}
102102

103-
async register(email: string, password: string, name: string, inviteToken?: string) {
104-
const body: Record<string, string> = { email, password, name };
103+
async register(email: string, password: string, name: string, inviteToken?: string, role?: string) {
104+
const body: Record<string, string> = { email, password, name, role: role ?? 'parent' };
105105
if (inviteToken) body.inviteToken = inviteToken;
106106

107107
const response = await this.post<any>(

client/src/pages/Family.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface SharedAccount {
3131
name: string;
3232
email: string;
3333
is_owner: boolean;
34+
role?: string;
3435
}
3536

3637
const Family: React.FC = () => {
@@ -408,7 +409,7 @@ const Family: React.FC = () => {
408409
<p className="text-body-sm text-muted-foreground truncate">{account.email}</p>
409410
</div>
410411
<Badge variant={account.is_owner ? 'primary' : 'default'}>
411-
{account.is_owner ? 'Propriétaire' : 'Membre'}
412+
{account.is_owner ? 'Propriétaire' : (account.role === 'enfant' ? '🧒 Enfant' : '👨 Parent')}
412413
</Badge>
413414
{/* Owner can kick non-owner members (except themselves) */}
414415
{isOwner && !account.is_owner && (

client/src/pages/Login.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const Login: React.FC = () => {
1313
const [email, setEmail] = useState('');
1414
const [password, setPassword] = useState('');
1515
const [name, setName] = useState('');
16+
const [role, setRole] = useState<'parent' | 'enfant'>('parent');
1617
const [error, setError] = useState('');
1718
const [loading, setLoading] = useState(false);
1819
const [inviteToken, setInviteToken] = useState<string | null>(null);
@@ -36,7 +37,7 @@ const Login: React.FC = () => {
3637
if (isLogin) {
3738
await login(email, password);
3839
} else {
39-
await register(email, password, name, inviteToken ?? undefined);
40+
await register(email, password, name, inviteToken ?? undefined, role);
4041
}
4142
} catch (err: unknown) {
4243
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
@@ -91,6 +92,38 @@ const Login: React.FC = () => {
9192
</div>
9293
)}
9394

95+
{!isLogin && (
96+
<div className="space-y-1.5">
97+
<label className="text-label-sm font-medium text-foreground block">Rôle dans la famille</label>
98+
<div className="grid grid-cols-2 gap-2">
99+
<button
100+
type="button"
101+
onClick={() => setRole('parent')}
102+
className={`flex flex-col items-center gap-1.5 p-3 rounded-nexus border-2 transition-colors ${
103+
role === 'parent'
104+
? 'border-nexus-blue bg-nexus-blue/10 text-nexus-blue'
105+
: 'border-border text-muted-foreground hover:border-nexus-blue/50'
106+
}`}
107+
>
108+
<span className="text-2xl">👨</span>
109+
<span className="text-label-sm font-semibold">Parent</span>
110+
</button>
111+
<button
112+
type="button"
113+
onClick={() => setRole('enfant')}
114+
className={`flex flex-col items-center gap-1.5 p-3 rounded-nexus border-2 transition-colors ${
115+
role === 'enfant'
116+
? 'border-nexus-blue bg-nexus-blue/10 text-nexus-blue'
117+
: 'border-border text-muted-foreground hover:border-nexus-blue/50'
118+
}`}
119+
>
120+
<span className="text-2xl">🧒</span>
121+
<span className="text-label-sm font-semibold">Enfant</span>
122+
</button>
123+
</div>
124+
</div>
125+
)}
126+
94127
<div className="space-y-1.5">
95128
<Input
96129
label="Email"

client/src/pages/ShoppingList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ const ShoppingList: React.FC = () => {
454454
<div className="mt-1 flex flex-wrap items-center gap-2 text-micro">
455455
<span className="rounded-pill bg-primary-soft px-2 py-0.5 text-primary">{item.category}</span>
456456
{item.quantity ? <span className="text-muted-foreground">Qt: {item.quantity}</span> : null}
457-
{item.price ? <span className="text-muted-foreground">{item.price.toFixed(2)} EUR</span> : null}
457+
{item.price ? <span className="text-muted-foreground">{Number(item.price).toFixed(2)} EUR</span> : null}
458458
</div>
459459
</div>
460460

server/src/db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ export const runMigrations = async () => {
141141
)`,
142142
'CREATE INDEX IF NOT EXISTS idx_family_invites_token ON family_invites(token)',
143143
'CREATE INDEX IF NOT EXISTS idx_family_invites_owner ON family_invites(owner_id)',
144+
// Migration 003: user role (parent / enfant)
145+
"ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) DEFAULT 'parent'",
144146
];
145147

146148
for (const migration of migrations) {

server/src/routes/auth.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ router.get('/me', authMiddleware, async (req: AuthRequest, res) => {
1010
try {
1111
// Use actualUserId so members see their own profile, not the owner's
1212
const result = await query(
13-
'SELECT id, email, name, (family_owner_id IS NULL) AS is_owner FROM users WHERE id = $1',
13+
'SELECT id, email, name, role, (family_owner_id IS NULL) AS is_owner FROM users WHERE id = $1',
1414
[req.actualUserId]
1515
);
1616
if (result.rows.length === 0) {
@@ -31,9 +31,10 @@ router.post('/register', async (req, res) => {
3131
}
3232

3333
try {
34-
const { email, password, name, inviteToken } = req.body;
34+
const { email, password, name, inviteToken, role } = req.body;
3535
const normalizedEmail = typeof email === 'string' ? normalizeEmail(email) : '';
3636
const cleanedName = typeof name === 'string' ? name.trim() : '';
37+
const cleanedRole = ['parent', 'enfant'].includes(role) ? role : 'parent';
3738

3839
if (!normalizedEmail || !password || !cleanedName) {
3940
return res.status(400).json({ success: false, error: 'Missing required fields' });
@@ -54,8 +55,8 @@ router.post('/register', async (req, res) => {
5455

5556
// Create user
5657
const result = await query(
57-
'INSERT INTO users (email, password_hash, name) VALUES ($1, $2, $3) RETURNING id, email, name',
58-
[normalizedEmail, password_hash, cleanedName]
58+
'INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, name, role',
59+
[normalizedEmail, password_hash, cleanedName, cleanedRole]
5960
);
6061

6162
const user = result.rows[0];
@@ -74,12 +75,23 @@ router.post('/register', async (req, res) => {
7475
await query('UPDATE users SET family_owner_id = $1 WHERE id = $2', [invite.owner_id, user.id]);
7576
await query("UPDATE family_invites SET status = 'accepted' WHERE id = $1", [invite.id]);
7677
}
78+
} else {
79+
// Auto-join: if a family owner already exists, automatically join that family
80+
const ownerResult = await query(
81+
'SELECT id FROM users WHERE family_owner_id IS NULL AND id != $1 ORDER BY created_at ASC LIMIT 1',
82+
[user.id]
83+
);
84+
if (ownerResult.rows.length > 0) {
85+
const existingOwner = ownerResult.rows[0] as { id: string };
86+
ownerId = existingOwner.id;
87+
await query('UPDATE users SET family_owner_id = $1 WHERE id = $2', [existingOwner.id, user.id]);
88+
}
7789
}
7890

7991
const token = generateToken(user.id, ownerId);
8092
const isOwner = ownerId === user.id;
8193

82-
res.json({ success: true, data: { user: { ...user, is_owner: isOwner }, token } });
94+
res.json({ success: true, data: { user: { ...user, is_owner: isOwner, role: user.role }, token } });
8395
} catch (error) {
8496
console.error('Register error:', error);
8597
res.status(500).json({ success: false, error: 'Internal server error' });
@@ -119,7 +131,7 @@ router.post('/login', async (req, res) => {
119131
res.json({
120132
success: true,
121133
data: {
122-
user: { id: user.id, email: user.email, name: user.name, is_owner: isOwner },
134+
user: { id: user.id, email: user.email, name: user.name, role: user.role, is_owner: isOwner },
123135
token
124136
}
125137
});

server/src/routes/familyInvites.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ router.use(authMiddleware);
3434
// GET /members — list all user accounts in this family
3535
router.get('/members', async (req: AuthRequest, res) => {
3636
const { rows } = await query(
37-
`SELECT id, name, email, (family_owner_id IS NULL) AS is_owner, created_at
37+
`SELECT id, name, email, role, (family_owner_id IS NULL) AS is_owner, created_at
3838
FROM users
3939
WHERE id = $1
4040
OR family_owner_id = $1

0 commit comments

Comments
 (0)