Skip to content

Commit c19bf01

Browse files
committed
feat(academy): add course deletion button in admin modal and update academy UI
1 parent 1862ad5 commit c19bf01

13 files changed

Lines changed: 616 additions & 609 deletions

File tree

‎src/App.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,26 @@ export default function App() {
516516
</ProtectedRoute>
517517
}
518518
/>
519+
<Route
520+
path="/academia/dashboard/programas"
521+
element={
522+
<ProtectedRoute allowedRoles={['distribuidor', 'admin', 'operaciones']}>
523+
<AcademyLayout>
524+
<Programas />
525+
</AcademyLayout>
526+
</ProtectedRoute>
527+
}
528+
/>
529+
<Route
530+
path="/academia/dashboard/programas/:slug"
531+
element={
532+
<ProtectedRoute allowedRoles={['distribuidor', 'admin', 'operaciones']}>
533+
<AcademyLayout>
534+
<ProgramDetalle />
535+
</AcademyLayout>
536+
</ProtectedRoute>
537+
}
538+
/>
519539
<Route
520540
path="/academia/dashboard/biblioteca"
521541
element={

‎src/components/AcademyLayout.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const navItems: NavItem[] = [
3232
{ label: 'Inicio', to: '/academia/dashboard', icon: <GraduationCap size={18} />, end: true },
3333
{ label: 'Mis Cursos', to: '/academia/dashboard/cursos', icon: <BookOpen size={18} /> },
3434
{ label: 'Explorar Cursos', to: '/academia/dashboard/explorar', icon: <Search size={18} /> },
35-
{ label: 'Programas', to: '/academia/programas', icon: <Layers size={18} /> },
35+
{ label: 'Programas', to: '/academia/dashboard/programas', icon: <Layers size={18} /> },
3636
{ label: 'Mis Recetas', to: '/academia/dashboard/recetas', icon: <Leaf size={18} /> },
3737
{ label: 'Certificaciones', to: '/academia/dashboard/diplomas', icon: <Award size={18} /> },
3838
{ label: 'Mi Perfil', to: '/academia/dashboard/perfil', icon: <User size={18} /> },

‎src/lib/academy-types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export interface AcademyLesson {
8282
sort_order: number;
8383
is_published: boolean;
8484
is_free_preview: boolean;
85-
is_required: boolean;
85+
is_required?: boolean;
8686
estimated_minutes: number | null;
8787
metadata: Record<string, any>;
8888
created_at: string;

‎src/lib/academy.ts‎

Lines changed: 122 additions & 565 deletions
Large diffs are not rendered by default.

‎src/lib/auth.tsx‎

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,105 @@ interface AuthContextValue {
3030
homeForProfile: (p: Profile | null) => '/admin' | '/operaciones' | '/dashboard' | '/academia/dashboard' | '/login';
3131
}
3232

33+
export const LOCAL_MOCK_ADMIN: Profile = {
34+
id: '16092a90-3e8e-466d-a509-eb6b074a92cd',
35+
codigo_distribuidor: 'SUMAK-00001',
36+
username: 'admin',
37+
nombre_completo: 'Administrador Sumak',
38+
cedula: '0999999999',
39+
email: 'admin@sumak.com',
40+
telefono: '0988447019',
41+
direccion: 'Oficina Central Sumak',
42+
ciudad: 'Babahoyo',
43+
codigo_patrocinador: null,
44+
patrocinador_id: null,
45+
paquete: 'lider',
46+
puntos: 9999,
47+
estado: 'activo',
48+
rol: 'admin',
49+
avatar_url: null,
50+
fecha_registro: '2026-01-01T00:00:00Z',
51+
fecha_aprobacion: '2026-01-01T00:00:00Z',
52+
has_completed_onboarding: true,
53+
};
54+
55+
export const LOCAL_MOCK_ADMIN_USER: User = {
56+
id: '16092a90-3e8e-466d-a509-eb6b074a92cd',
57+
app_metadata: {},
58+
user_metadata: {
59+
nombre: 'Administrador Sumak',
60+
rol: 'admin',
61+
},
62+
aud: 'authenticated',
63+
created_at: '2026-01-01T00:00:00Z',
64+
} as User;
65+
66+
export const LOCAL_MOCK_USER: Profile = {
67+
id: '22222222-2222-4222-8222-222222222222',
68+
codigo_distribuidor: 'SUMAK-00123',
69+
username: 'user',
70+
nombre_completo: 'Usuario Distribuidor',
71+
cedula: '0988888888',
72+
email: 'user@sumak.com',
73+
telefono: '0991234567',
74+
direccion: 'Av. Principal 123',
75+
ciudad: 'Guayaquil',
76+
codigo_patrocinador: 'SUMAK-00001',
77+
patrocinador_id: '16092a90-3e8e-466d-a509-eb6b074a92cd',
78+
paquete: 'emprendedor',
79+
puntos: 150,
80+
estado: 'activo',
81+
rol: 'distribuidor',
82+
avatar_url: null,
83+
fecha_registro: '2026-01-01T00:00:00Z',
84+
fecha_aprobacion: '2026-01-01T00:00:00Z',
85+
has_completed_onboarding: true,
86+
};
87+
88+
export const LOCAL_MOCK_USER_AUTH: User = {
89+
id: '22222222-2222-4222-8222-222222222222',
90+
app_metadata: {},
91+
user_metadata: {
92+
nombre: 'Usuario Distribuidor',
93+
rol: 'distribuidor',
94+
},
95+
aud: 'authenticated',
96+
created_at: '2026-01-01T00:00:00Z',
97+
} as User;
98+
99+
export const LOCAL_MOCK_ESTUDIANTE: Profile = {
100+
id: '33333333-3333-4333-8333-333333333333',
101+
codigo_distribuidor: 'ACE-00088',
102+
username: 'estudiante',
103+
nombre_completo: 'Estudiante Academia Sumak',
104+
cedula: '0977777777',
105+
email: 'estudiante@sumak.com',
106+
telefono: '0997654321',
107+
direccion: 'Campus Virtual',
108+
ciudad: 'Quito',
109+
codigo_patrocinador: null,
110+
patrocinador_id: null,
111+
paquete: null,
112+
puntos: 0,
113+
estado: 'activo',
114+
rol: 'distribuidor',
115+
avatar_url: null,
116+
fecha_registro: '2026-01-01T00:00:00Z',
117+
fecha_aprobacion: '2026-01-01T00:00:00Z',
118+
has_completed_onboarding: true,
119+
};
120+
121+
export const LOCAL_MOCK_ESTUDIANTE_AUTH: User = {
122+
id: '33333333-3333-4333-8333-333333333333',
123+
app_metadata: {},
124+
user_metadata: {
125+
nombre: 'Estudiante Academia Sumak',
126+
rol: 'estudiante',
127+
},
128+
aud: 'authenticated',
129+
created_at: '2026-01-01T00:00:00Z',
130+
} as User;
131+
33132
const AuthContext = createContext<AuthContextValue | null>(null);
34133

35134
export function AuthProvider({ children }: { children: React.ReactNode }) {
@@ -59,6 +158,33 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
59158
}
60159

61160
useEffect(() => {
161+
// Check for local mock session first (dev only)
162+
if (import.meta.env.DEV) {
163+
const mockRole = localStorage.getItem('sumak_local_mock_role');
164+
const isMockAdmin = localStorage.getItem('sumak_local_mock_admin') === 'true';
165+
166+
if (mockRole === 'admin' || isMockAdmin) {
167+
setUser(LOCAL_MOCK_ADMIN_USER);
168+
setProfile(LOCAL_MOCK_ADMIN);
169+
setLoading(false);
170+
return;
171+
}
172+
173+
if (mockRole === 'user') {
174+
setUser(LOCAL_MOCK_USER_AUTH);
175+
setProfile(LOCAL_MOCK_USER);
176+
setLoading(false);
177+
return;
178+
}
179+
180+
if (mockRole === 'estudiante') {
181+
setUser(LOCAL_MOCK_ESTUDIANTE_AUTH);
182+
setProfile(LOCAL_MOCK_ESTUDIANTE);
183+
setLoading(false);
184+
return;
185+
}
186+
}
187+
62188
// Get initial session
63189
supabase.auth.getSession().then(({ data: { session } }) => {
64190
setUser(session?.user ?? null);
@@ -71,6 +197,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
71197

72198
// Listen for auth changes
73199
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
200+
if (import.meta.env.DEV && (localStorage.getItem('sumak_local_mock_role') || localStorage.getItem('sumak_local_mock_admin'))) return;
74201
setUser(session?.user ?? null);
75202
if (session?.user) {
76203
fetchProfile(session.user.id);
@@ -85,6 +212,35 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
85212
async function signIn(
86213
email: string, password: string, captchaToken?: string,
87214
): Promise<{ error: string | null; profile: Profile | null }> {
215+
const trimmed = email.trim().toLowerCase();
216+
217+
// Bypass credenciales locales (dev only)
218+
if (import.meta.env.DEV) {
219+
if ((trimmed === 'admin' || trimmed === 'admin@sumak.com' || trimmed === 'admin@sumak.ec' || trimmed === 'admin@sumak.local') && password === 'admin') {
220+
localStorage.setItem('sumak_local_mock_role', 'admin');
221+
localStorage.setItem('sumak_local_mock_admin', 'true');
222+
setUser(LOCAL_MOCK_ADMIN_USER);
223+
setProfile(LOCAL_MOCK_ADMIN);
224+
return { error: null, profile: LOCAL_MOCK_ADMIN };
225+
}
226+
227+
if ((trimmed === 'user' || trimmed === 'user@sumak.com' || trimmed === 'user@sumak.ec' || trimmed === 'user@sumak.local') && password === 'user') {
228+
localStorage.setItem('sumak_local_mock_role', 'user');
229+
localStorage.setItem('sumak_local_mock_admin', 'false');
230+
setUser(LOCAL_MOCK_USER_AUTH);
231+
setProfile(LOCAL_MOCK_USER);
232+
return { error: null, profile: LOCAL_MOCK_USER };
233+
}
234+
235+
if ((trimmed === 'estudiante' || trimmed === 'estudiante@sumak.com' || trimmed === 'estudiante@sumak.ec' || trimmed === 'estudiante@sumak.local') && password === 'estudiante') {
236+
localStorage.setItem('sumak_local_mock_role', 'estudiante');
237+
localStorage.setItem('sumak_local_mock_admin', 'false');
238+
setUser(LOCAL_MOCK_ESTUDIANTE_AUTH);
239+
setProfile(LOCAL_MOCK_ESTUDIANTE);
240+
return { error: null, profile: LOCAL_MOCK_ESTUDIANTE };
241+
}
242+
}
243+
88244
const { data, error } = await supabase.auth.signInWithPassword({
89245
email,
90246
password,
@@ -113,7 +269,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
113269
if (session?.user) await fetchProfile(session.user.id);
114270
}
115271

116-
const isMockUser = false;
272+
const isMockUser = Boolean(
273+
user?.id === LOCAL_MOCK_ADMIN.id ||
274+
user?.id === LOCAL_MOCK_USER.id ||
275+
user?.id === LOCAL_MOCK_ESTUDIANTE.id ||
276+
localStorage.getItem('sumak_local_mock_role') ||
277+
localStorage.getItem('sumak_local_mock_admin')
278+
);
117279

118280
async function completeOnboarding(): Promise<void> {
119281
if (!profile) return;

‎src/pages/Login.tsx‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ export default function Login() {
3232
async function resolveEmail(value: string): Promise<string | null> {
3333
const trimmed = value.trim();
3434
if (!trimmed) return null;
35+
const lower = trimmed.toLowerCase();
36+
if (['admin', 'user', 'estudiante'].includes(lower)) {
37+
return lower;
38+
}
3539
// Si parece email, lo usamos tal cual.
3640
if (trimmed.includes('@')) return trimmed;
3741
// Si no, lo tratamos como username y traducimos a email.
3842
const { data, error: rpcError } = await supabase.rpc('get_email_for_login', {
39-
p_username: trimmed.toLowerCase(),
43+
p_username: lower,
4044
});
4145
if (rpcError || !data) return null;
4246
return data as string;
@@ -196,6 +200,46 @@ export default function Login() {
196200
Protegido por Cloudflare Turnstile
197201
</p>
198202
)}
203+
{import.meta.env.DEV && (
204+
<div className="pt-2 border-t border-[#C8D8CB]/40 space-y-2">
205+
<p className="text-[11px] text-[#6B7280] font-medium text-center">Acceso Rápido Local (Dev):</p>
206+
<div className="grid grid-cols-3 gap-1.5">
207+
<button
208+
type="button"
209+
onClick={() => {
210+
setIdentifier('admin');
211+
setPassword('admin');
212+
}}
213+
className="py-2 px-1 rounded-xl border border-dashed border-[#1A4E26]/40 text-[#1A4E26] font-semibold text-xs hover:bg-[#EBF4ED] transition-colors text-center"
214+
title="Administrador (admin / admin)"
215+
>
216+
🛠️ admin
217+
</button>
218+
<button
219+
type="button"
220+
onClick={() => {
221+
setIdentifier('user');
222+
setPassword('user');
223+
}}
224+
className="py-2 px-1 rounded-xl border border-dashed border-[#1A4E26]/40 text-[#1A4E26] font-semibold text-xs hover:bg-[#EBF4ED] transition-colors text-center"
225+
title="Usuario Distribuidor (user / user)"
226+
>
227+
👤 user
228+
</button>
229+
<button
230+
type="button"
231+
onClick={() => {
232+
setIdentifier('estudiante');
233+
setPassword('estudiante');
234+
}}
235+
className="py-2 px-1 rounded-xl border border-dashed border-[#1A4E26]/40 text-[#1A4E26] font-semibold text-xs hover:bg-[#EBF4ED] transition-colors text-center"
236+
title="Estudiante Academia (estudiante / estudiante)"
237+
>
238+
🎓 estudiante
239+
</button>
240+
</div>
241+
</div>
242+
)}
199243
</form>
200244
)}
201245

‎src/pages/academia/LoginAcademia.tsx‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ export default function LoginAcademia() {
2525
async function resolveEmail(value: string): Promise<string | null> {
2626
const trimmed = value.trim();
2727
if (!trimmed) return null;
28+
const lower = trimmed.toLowerCase();
29+
if (['admin', 'user', 'estudiante'].includes(lower)) {
30+
return lower;
31+
}
2832
if (trimmed.includes('@')) return trimmed;
2933
// Intentar resolver como username
30-
const { data } = await supabase.rpc('get_email_for_login', { p_username: trimmed.toLowerCase() });
34+
const { data } = await supabase.rpc('get_email_for_login', { p_username: lower });
3135
return data as string | null;
3236
}
3337

@@ -166,6 +170,47 @@ export default function LoginAcademia() {
166170
<><ArrowRight size={20} /> Ingresar a mi Academia</>
167171
)}
168172
</button>
173+
174+
{import.meta.env.DEV && (
175+
<div className="pt-2 border-t border-white/10 space-y-2">
176+
<p className="text-[11px] text-white/50 font-medium text-center">Acceso Rápido Local (Dev):</p>
177+
<div className="grid grid-cols-3 gap-1.5">
178+
<button
179+
type="button"
180+
onClick={() => {
181+
setEmail('estudiante');
182+
setPassword('estudiante');
183+
}}
184+
className="py-2 px-1 rounded-xl border border-dashed border-[#D4AF37]/50 text-[#D4AF37] font-semibold text-xs hover:bg-[#D4AF37]/10 transition-colors text-center"
185+
title="Estudiante Academia (estudiante / estudiante)"
186+
>
187+
🎓 estudiante
188+
</button>
189+
<button
190+
type="button"
191+
onClick={() => {
192+
setEmail('user');
193+
setPassword('user');
194+
}}
195+
className="py-2 px-1 rounded-xl border border-dashed border-white/20 text-white/70 font-medium text-xs hover:bg-white/5 transition-colors text-center"
196+
title="Usuario Distribuidor (user / user)"
197+
>
198+
👤 user
199+
</button>
200+
<button
201+
type="button"
202+
onClick={() => {
203+
setEmail('admin');
204+
setPassword('admin');
205+
}}
206+
className="py-2 px-1 rounded-xl border border-dashed border-white/20 text-white/70 font-medium text-xs hover:bg-white/5 transition-colors text-center"
207+
title="Administrador (admin / admin)"
208+
>
209+
🛠️ admin
210+
</button>
211+
</div>
212+
</div>
213+
)}
169214
</form>
170215
)}
171216

0 commit comments

Comments
 (0)