Skip to content

Commit 0a38144

Browse files
authored
Merge pull request #40 from fuzziecoder/codex/fix-notification-issue-and-update-admin-management
Add org-code multi-management scoping and fix notification fan-out
2 parents 90eaa41 + 4c0eb46 commit 0a38144

11 files changed

Lines changed: 158 additions & 34 deletions

contexts/AuthContext.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { supabase } from "../services/supabase";
1616
interface AuthContextType {
1717
user: User | null;
1818
profile: UserProfile | null;
19-
login: (identifier: string, password: string) => Promise<void>;
19+
login: (identifier: string, password: string, orgCode?: string) => Promise<void>;
2020
logout: () => void;
2121
updateProfile: (updates: Partial<UserProfile>) => Promise<void>;
2222
loading: boolean;
@@ -54,20 +54,25 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
5454
/* Login */
5555
/* ------------------------------------------------------------------------ */
5656

57-
const login = async (identifier: string, password: string) => {
57+
const login = async (identifier: string, password: string, orgCode?: string) => {
5858
setLoading(true);
5959

6060
try {
6161
// Strip formatting from phone number if it looks like a phone
6262
const cleanIdentifier = identifier.replace(/\D/g, '').length === 10 ? identifier.replace(/\D/g, '') : identifier;
6363

6464
// First try to get profile from Supabase
65-
const { data: profiles, error: profileError } = await supabase
65+
let profileQuery = supabase
6666
.from('profiles')
6767
.select('*')
6868
.or(`email.eq.${identifier},phone.eq.${cleanIdentifier},username.eq.${identifier}`)
69-
.eq('password', password)
70-
.single();
69+
.eq('password', password);
70+
71+
if (orgCode?.trim()) {
72+
profileQuery = profileQuery.eq('org_code', orgCode.trim());
73+
}
74+
75+
const { data: profiles, error: profileError } = await profileQuery.single();
7176

7277
if (!profileError && profiles) {
7378
// Found in Supabase - use UUID from database
@@ -94,7 +99,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
9499

95100
// Fallback to mockApi for development
96101
const { user: loggedInUser, profile: userProfile } =
97-
await mockApi.login(identifier, password);
102+
await mockApi.login(identifier, password, orgCode);
98103

99104
// If using mockApi, try to find the UUID in Supabase
100105
try {

pages/DrinksPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ const DrinksPage: React.FC = () => {
126126

127127
try {
128128
setPageError(null);
129-
const spotData = await spotService.getUpcomingSpot();
129+
const spotData = await spotService.getUpcomingSpot(profile?.org_code);
130130
setSpot(spotData);
131131

132132
if (spotData) {

pages/DrinksPageNew.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const DrinksPage: React.FC = () => {
7979
if (!profile) return;
8080

8181
try {
82-
const spotData = await spotService.getUpcomingSpot();
82+
const spotData = await spotService.getUpcomingSpot(profile?.org_code);
8383
setSpot(spotData);
8484

8585
if (spotData) {

pages/HomePage.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ const HomePage: React.FC = () => {
7373
}
7474
};
7575
checkSetup();
76-
}, []);
76+
}, [profile?.org_code]);
7777

7878
/* ----------------------------- FETCH DATA ----------------------------- */
7979

8080
const fetchData = useCallback(async () => {
8181
setLoading(true);
8282
setDbSetupError(null);
8383
try {
84-
const spotData = await spotService.getUpcomingSpot();
84+
const spotData = await spotService.getUpcomingSpot(profile?.org_code);
8585
setSpot(spotData);
8686

8787
if (spotData) {
@@ -101,7 +101,7 @@ const HomePage: React.FC = () => {
101101
} finally {
102102
setLoading(false);
103103
}
104-
}, []);
104+
}, [profile?.org_code]);
105105

106106
useEffect(() => {
107107
fetchData();
@@ -119,7 +119,8 @@ const HomePage: React.FC = () => {
119119
// Notify all users in database
120120
notificationService.createNotificationForAllUsers(
121121
"New Spot Created!",
122-
`A new spot has been created at ${payload.new.location}`
122+
`A new spot has been created at ${payload.new.location}`,
123+
profile?.org_code
123124
).catch(err => console.error('Error notifying all users:', err));
124125
// Also notify current user locally
125126
notify("New Spot Created!", `A new spot has been created at ${payload.new.location}`);
@@ -142,7 +143,8 @@ const HomePage: React.FC = () => {
142143
// Notify all users about RSVP updates
143144
notificationService.createNotificationForAllUsers(
144145
"RSVP Updated",
145-
`Someone ${statusMessages[payload.new.status] || 'updated their RSVP'}`
146+
`Someone ${statusMessages[payload.new.status] || 'updated their RSVP'}`,
147+
profile?.org_code
146148
).catch(err => console.error('Error notifying all users:', err));
147149
// Also notify current user locally
148150
notify("RSVP Updated", `Someone ${statusMessages[payload.new.status] || 'updated their RSVP'}`);
@@ -329,13 +331,20 @@ const HomePage: React.FC = () => {
329331
created_by: userId,
330332
latitude: newSpotData.latitude,
331333
longitude: newSpotData.longitude,
334+
org_code: profile.org_code,
332335
});
333336

334337
// Get all users to create invitations for them
335-
const { data: allUsers, error: usersError } = await supabase
338+
let usersQuery = supabase
336339
.from('profiles')
337340
.select('id');
338341

342+
if (profile.org_code) {
343+
usersQuery = usersQuery.eq('org_code', profile.org_code);
344+
}
345+
346+
const { data: allUsers, error: usersError } = await usersQuery;
347+
339348
if (!usersError && allUsers) {
340349
// Create invitations for all users (pending status by default)
341350
const invitationPromises = allUsers.map((user) =>
@@ -359,7 +368,8 @@ const HomePage: React.FC = () => {
359368
// Notify all users about the new spot
360369
await notificationService.createNotificationForAllUsers(
361370
"New Spot Created!",
362-
`A new spot has been created at ${newSpotData.location} on ${new Date(newSpotData.date).toLocaleDateString()}`
371+
`A new spot has been created at ${newSpotData.location} on ${new Date(newSpotData.date).toLocaleDateString()}`,
372+
profile.org_code
363373
);
364374
// Also notify current user locally
365375
notify("New Spot Created!", `A new spot has been created at ${newSpotData.location} on ${new Date(newSpotData.date).toLocaleDateString()}`);
@@ -588,7 +598,7 @@ const HomePage: React.FC = () => {
588598
setIsEditSpotModalOpen(false);
589599
setEditingSpot(null);
590600
await fetchData();
591-
await notificationService.createNotificationForAllUsers("Spot Updated", "Spot details have been updated successfully");
601+
await notificationService.createNotificationForAllUsers("Spot Updated", "Spot details have been updated successfully", profile?.org_code);
592602
notify("Spot Updated", "Spot details have been updated successfully");
593603
} catch (error: any) {
594604
alert(`Failed to update spot: ${error.message || 'Please try again.'}`);
@@ -600,7 +610,7 @@ const HomePage: React.FC = () => {
600610
try {
601611
await spotService.deleteSpot(spotId);
602612
await fetchData();
603-
await notificationService.createNotificationForAllUsers("Spot Deleted", "Spot has been deleted successfully");
613+
await notificationService.createNotificationForAllUsers("Spot Deleted", "Spot has been deleted successfully", profile?.org_code);
604614
notify("Spot Deleted", "Spot has been deleted successfully");
605615
} catch (error: any) {
606616
alert(`Failed to delete spot: ${error.message || 'Please try again.'}`);

pages/LoginPage.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type FormData = {
2424
mobileNumber: string;
2525
email: string;
2626
username: string;
27+
orgCode: string;
28+
managementName: string;
2729
};
2830

2931
const formatMobile = (val: string) => {
@@ -53,6 +55,8 @@ const LoginPage: React.FC = () => {
5355
newPassword: '',
5456
email: '',
5557
username: '',
58+
orgCode: '',
59+
managementName: '',
5660
});
5761

5862
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
@@ -110,6 +114,13 @@ const LoginPage: React.FC = () => {
110114
case 'username':
111115
if (!value.trim()) error = 'Username is required';
112116
break;
117+
case 'orgCode':
118+
if (!value.trim()) error = 'Org Code is required';
119+
else if (!/^\d{4}$/.test(value.trim())) error = 'Org Code must be 4 digits';
120+
break;
121+
case 'managementName':
122+
if (view === 'mobile-setup' && !value.trim()) error = 'Management name is required';
123+
break;
113124
}
114125
return error;
115126
};
@@ -134,7 +145,7 @@ const LoginPage: React.FC = () => {
134145
const handleLogin = async (e: React.FormEvent) => {
135146
e.preventDefault();
136147
clearMessages();
137-
const fields = authMethod === 'email' ? ['loginEmail', 'loginPassword'] : ['loginMobile', 'loginPassword'];
148+
const fields = authMethod === 'email' ? ['loginEmail', 'loginPassword', 'orgCode'] : ['loginMobile', 'loginPassword', 'orgCode'];
138149
let isValid = true;
139150
const newErrors: any = {};
140151
fields.forEach(f => {
@@ -147,7 +158,7 @@ const LoginPage: React.FC = () => {
147158
// Strip formatting from phone number for login
148159
let identifier = authMethod === 'email' ? formData.loginEmail : formData.loginMobile.replace(/\D/g, '');
149160
try {
150-
await login(identifier, formData.loginPassword);
161+
await login(identifier, formData.loginPassword, formData.orgCode.trim());
151162
navigate('/dashboard/home');
152163
} catch (err: any) {
153164
setApiError(err.message || 'Login failed.');
@@ -228,7 +239,9 @@ const LoginPage: React.FC = () => {
228239
const e1 = validateField('email', formData.email);
229240
const e2 = validateField('username', formData.username);
230241
const e3 = validateField('newPassword', formData.newPassword);
231-
if (e1 || e2 || e3) { setErrors({ email: e1, username: e2, newPassword: e3 }); return; }
242+
const e4 = validateField('orgCode', formData.orgCode);
243+
const e5 = validateField('managementName', formData.managementName);
244+
if (e1 || e2 || e3 || e4 || e5) { setErrors({ email: e1, username: e2, newPassword: e3, orgCode: e4, managementName: e5 }); return; }
232245

233246
// Check username uniqueness
234247
try {
@@ -255,10 +268,12 @@ const LoginPage: React.FC = () => {
255268
password: formData.newPassword,
256269
profile_pic_url: DEFAULT_AVATARS[Math.floor(Math.random() * DEFAULT_AVATARS.length)],
257270
role: 'user',
271+
org_code: formData.orgCode.trim(),
272+
management_name: formData.managementName.trim(),
258273
});
259274

260275
// Auto-login after registration
261-
await login(phoneDigits, formData.newPassword);
276+
await login(phoneDigits, formData.newPassword, formData.orgCode.trim());
262277
setSuccess('Account created! Welcome to the squad.');
263278
setTimeout(() => navigate('/dashboard/home'), 1500);
264279
} catch (err: any) {
@@ -304,6 +319,7 @@ const LoginPage: React.FC = () => {
304319
<div className="absolute right-4 top-10 text-orange-400 flex items-center gap-1"><AlertCircle size={14} /><span className="text-[10px] font-bold">FORMAT?</span></div>
305320
)}
306321
</div>
322+
<Input name="orgCode" label="Org Code" type="text" value={formData.orgCode} onChange={handleChange} error={errors.orgCode} icon={<User size={16} />} placeholder="4-digit code" />
307323
<Input name="loginPassword" label="Password" type={showPassword ? 'text' : 'password'} value={formData.loginPassword} onChange={handleChange} error={errors.loginPassword} icon={<Lock size={16} />} rightIcon={showPassword ? <EyeOff size={16} /> : <Eye size={16} />} onRightIconClick={() => setShowPassword(!showPassword)} />
308324
<div className="flex justify-end px-1"><button type="button" onClick={() => setView('forgot')} className="text-[10px] font-black uppercase tracking-widest text-zinc-500 hover:text-white transition-colors">Forgot Password?</button></div>
309325
<Button type="submit" className="w-full py-4 uppercase tracking-widest font-black" disabled={loading}>Enter Squad</Button>
@@ -317,6 +333,7 @@ const LoginPage: React.FC = () => {
317333
<h1 className="text-3xl font-black text-white tracking-tighter mb-8">MOBILE</h1>
318334
<form className="space-y-6" onSubmit={handleLogin} noValidate>
319335
<Input name="loginMobile" label="Phone" type="tel" value={formData.loginMobile} onChange={handleChange} error={errors.loginMobile} icon={<Smartphone size={16} />} placeholder="(000) 000-0000" />
336+
<Input name="orgCode" label="Org Code" type="text" value={formData.orgCode} onChange={handleChange} error={errors.orgCode} icon={<User size={16} />} placeholder="4-digit code" />
320337
<Input name="loginPassword" label="Password" type={showPassword ? 'text' : 'password'} value={formData.loginPassword} onChange={handleChange} error={errors.loginPassword} icon={<Lock size={16} />} rightIcon={showPassword ? <EyeOff size={16} /> : <Eye size={16} />} onRightIconClick={() => setShowPassword(!showPassword)} />
321338
<Button type="submit" className="w-full py-4 uppercase tracking-widest font-black" disabled={loading}>Join Meetup</Button>
322339
<div className="text-center"><button type="button" onClick={() => setView('mobile-register')} className="text-[10px] font-black uppercase tracking-widest text-zinc-500 hover:text-white transition-colors">Create Account</button></div>
@@ -366,6 +383,8 @@ const LoginPage: React.FC = () => {
366383
<form className="space-y-6" onSubmit={handleMobileSetup} noValidate>
367384
<Input name="email" label="Email Address" type="email" value={formData.email} onChange={handleChange} error={errors.email} icon={<Mail size={16} />} placeholder="john@doe.com" />
368385
<Input name="username" label="Username" type="text" value={formData.username} onChange={handleChange} error={errors.username} icon={<AtSign size={16} />} placeholder="johndoe" />
386+
<Input name="managementName" label="Management Name" type="text" value={formData.managementName} onChange={handleChange} error={errors.managementName} icon={<User size={16} />} placeholder="Team/Org name" />
387+
<Input name="orgCode" label="Org Code" type="text" value={formData.orgCode} onChange={handleChange} error={errors.orgCode} icon={<User size={16} />} placeholder="4-digit code" />
369388
<div className="space-y-3">
370389
<Input name="newPassword" label="Password" type="password" value={formData.newPassword} onChange={handleChange} error={errors.newPassword} icon={<Lock size={16} />} />
371390
<PasswordStrength password={formData.newPassword} />

pages/PaymentPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const PaymentPage: React.FC = () => {
3737

3838
const fetchData = useCallback(async () => {
3939
try {
40-
const spotData = await spotService.getUpcomingSpot();
40+
const spotData = await spotService.getUpcomingSpot(profile?.org_code);
4141
setSpot(spotData);
4242

4343
if (spotData) {

services/database.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ import { Spot, Invitation, Payment, InvitationStatus, PaymentStatus, UserProfile
77

88
export const spotService = {
99
// Get upcoming spot (date >= today)
10-
async getUpcomingSpot(): Promise<Spot | null> {
10+
async getUpcomingSpot(orgCode?: string): Promise<Spot | null> {
1111
const now = new Date();
1212
const today = now.toISOString().split('T')[0];
1313

14-
const { data, error } = await supabase
14+
let query = supabase
1515
.from('spots')
1616
.select('*')
1717
.gte('date', today)
1818
.order('date', { ascending: true })
19-
.limit(1)
20-
.maybeSingle();
19+
.limit(1);
20+
21+
if (orgCode?.trim()) {
22+
query = query.eq('org_code', orgCode.trim());
23+
}
24+
25+
const { data, error } = await query.maybeSingle();
2126

2227
if (error) {
2328
if (error.code === 'PGRST116') {
@@ -35,15 +40,21 @@ export const spotService = {
3540
},
3641

3742
// Get all upcoming spots
38-
async getUpcomingSpots(): Promise<Spot[]> {
43+
async getUpcomingSpots(orgCode?: string): Promise<Spot[]> {
3944
const today = new Date().toISOString().split('T')[0];
4045

41-
const { data, error } = await supabase
46+
let query = supabase
4247
.from('spots')
4348
.select('*')
4449
.gte('date', today)
4550
.order('date', { ascending: true });
4651

52+
if (orgCode?.trim()) {
53+
query = query.eq('org_code', orgCode.trim());
54+
}
55+
56+
const { data, error } = await query;
57+
4758
if (error) {
4859
console.error('Error fetching upcoming spots:', error);
4960
throw error;
@@ -53,15 +64,21 @@ export const spotService = {
5364
},
5465

5566
// Get past spots (date < today)
56-
async getPastSpots(): Promise<Spot[]> {
67+
async getPastSpots(orgCode?: string): Promise<Spot[]> {
5768
const today = new Date().toISOString().split('T')[0];
5869

59-
const { data, error } = await supabase
70+
let query = supabase
6071
.from('spots')
6172
.select('*')
6273
.lt('date', today)
6374
.order('date', { ascending: false });
6475

76+
if (orgCode?.trim()) {
77+
query = query.eq('org_code', orgCode.trim());
78+
}
79+
80+
const { data, error } = await query;
81+
6582
if (error) {
6683
if (error.message?.includes('relation') || error.message?.includes('does not exist')) {
6784
throw new Error('Database tables not found. Please run the SQL migration in Supabase SQL Editor. See supabase_migration.sql file.');
@@ -93,6 +110,7 @@ export const spotService = {
93110
created_by: spotData.created_by,
94111
description: spotData.description || '',
95112
feedback: spotData.feedback || '',
113+
org_code: spotData.org_code,
96114
latitude: spotData.latitude,
97115
longitude: spotData.longitude,
98116
})
@@ -464,6 +482,8 @@ export const profileService = {
464482
password: string;
465483
profile_pic_url?: string;
466484
role?: string;
485+
org_code?: string;
486+
management_name?: string;
467487
}): Promise<UserProfile> {
468488
const { data, error } = await supabase
469489
.from('profiles')
@@ -475,6 +495,8 @@ export const profileService = {
475495
password: profileData.password,
476496
profile_pic_url: profileData.profile_pic_url || 'https://api.dicebear.com/7.x/thumbs/svg?seed=default',
477497
role: profileData.role || 'user',
498+
org_code: profileData.org_code,
499+
management_name: profileData.management_name,
478500
location: 'Broville',
479501
is_verified: true,
480502
})
@@ -1103,13 +1125,19 @@ export const notificationService = {
11031125
},
11041126

11051127
// Create notifications for all users
1106-
async createNotificationForAllUsers(title: string, message: string): Promise<void> {
1128+
async createNotificationForAllUsers(title: string, message: string, orgCode?: string): Promise<void> {
11071129
try {
11081130
// Get all users
1109-
const { data: allUsers, error: usersError } = await supabase
1131+
let usersQuery = supabase
11101132
.from('profiles')
11111133
.select('id');
11121134

1135+
if (orgCode?.trim()) {
1136+
usersQuery = usersQuery.eq('org_code', orgCode.trim());
1137+
}
1138+
1139+
const { data: allUsers, error: usersError } = await usersQuery;
1140+
11131141
if (usersError || !allUsers || allUsers.length === 0) {
11141142
console.error('Error fetching users for notifications:', usersError);
11151143
return;

0 commit comments

Comments
 (0)