Skip to content

Commit 8ee28e2

Browse files
author
Celio Maximiano
committed
release: v1.3.1
1 parent dbc787a commit 8ee28e2

13 files changed

Lines changed: 291 additions & 11 deletions

File tree

backend/routes/students/create_student.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def create_student():
3030
name = data.get('name', '').strip()
3131
email = data.get('email', '').strip().lower()
3232
password = data.get('password', '').strip()
33+
phone = data.get('phone', '').strip()
3334
course_ids = data.get('courseIds', [])
3435

3536
if not name or not email or not password:
@@ -43,7 +44,7 @@ def create_student():
4344
hashed_password = generate_password_hash(password)
4445

4546
try:
46-
new_student = Student(email=email, password=hashed_password, name=name)
47+
new_student = Student(email=email, password=hashed_password, name=name, phone=phone)
4748
db.session.add(new_student)
4849
db.session.flush()
4950

@@ -62,10 +63,12 @@ def create_student():
6263
'id': new_student.id,
6364
'name': new_student.name,
6465
'email': new_student.email,
66+
'phone': new_student.phone or '',
6567
'status': 'active' if new_student.courses else 'inactive',
6668
'courses': [{'id': c.id, 'name': c.name} for c in new_student.courses],
6769
'createdAt': new_student.created_at.isoformat() if new_student.created_at else None,
6870
'quickAccessToken': new_student.uuid,
71+
'extra_data': new_student.extra_data or {},
6972
}
7073
})
7174

backend/routes/students/list_students.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ def list_students():
4242
if search:
4343
like = f'%{search}%'
4444
query = query.filter(
45-
or_(Student.name.ilike(like), Student.email.ilike(like))
45+
or_(
46+
Student.name.ilike(like),
47+
Student.email.ilike(like),
48+
Student.phone.ilike(like),
49+
Student.extra_data.cast(db.Text).ilike(like)
50+
)
4651
)
4752

4853
if course_id:
@@ -84,6 +89,7 @@ def _serialize(s: Student) -> dict:
8489
'courses': [{'id': c.id, 'name': c.name} for c in s.courses],
8590
'createdAt': s.created_at.isoformat() if s.created_at else None,
8691
'quickAccessToken': s.uuid,
92+
'extra_data': s.extra_data or {},
8793
}
8894

8995

backend/routes/students/update_student.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def update_student(student_id):
3838

3939
student.name = data.get('name', student.name).strip()
4040
student.email = new_email
41+
if 'phone' in data:
42+
student.phone = data.get('phone', '').strip()
4143

4244
new_password = data.get('password', '').strip()
4345
if new_password:
@@ -52,9 +54,11 @@ def update_student(student_id):
5254
'id': student.id,
5355
'name': student.name,
5456
'email': student.email,
57+
'phone': student.phone or '',
5558
'status': 'active' if student.courses else 'inactive',
5659
'courses': [{'id': c.id, 'name': c.name} for c in student.courses],
5760
'quickAccessToken': student.uuid,
61+
'extra_data': student.extra_data or {},
5862
}
5963
})
6064

backend/webhook/platforms/payt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def _extract_extra_data(data: dict) -> dict:
88
'seller_id': data.get('seller_id'),
99
'customer_code': data.get('customer', {}).get('code'),
1010
'payment_method': data.get('transaction', {}).get('payment_method'),
11+
'customer': data.get('customer', {}),
1112
}
1213

1314
# UTMs e source

frontend/src/components/modals/students/AddStudentModal.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface AddStudentFormData {
2424
name: string;
2525
email: string;
2626
password: string;
27+
phone?: string;
2728
courseIds: number[];
2829
}
2930

@@ -37,6 +38,7 @@ export function AddStudentModal({
3738
}: AddStudentModalProps) {
3839
const [name, setName] = useState("");
3940
const [email, setEmail] = useState("");
41+
const [phone, setPhone] = useState("");
4042
const [password, setPassword] = useState("");
4143
const [showPassword, setShowPassword] = useState(false);
4244
const [selectedCourseIds, setSelectedCourseIds] = useState<number[]>([]);
@@ -53,6 +55,7 @@ export function AddStudentModal({
5355
function resetForm() {
5456
setName("");
5557
setEmail("");
58+
setPhone("");
5659
setPassword("");
5760
setSelectedCourseIds([]);
5861
setCourseToAdd("");
@@ -108,7 +111,7 @@ export function AddStudentModal({
108111

109112
function handleSubmit(e: React.FormEvent) {
110113
e.preventDefault();
111-
onSubmit({ name, email, password, courseIds: selectedCourseIds });
114+
onSubmit({ name, email, password, phone, courseIds: selectedCourseIds });
112115
}
113116

114117
const canSubmit = name.trim() && email.trim() && password.trim() && !hasEmailError;
@@ -177,6 +180,19 @@ export function AddStudentModal({
177180
)}
178181
</div>
179182

183+
{/* Phone */}
184+
<div className="space-y-2">
185+
<Label htmlFor="student-phone" className="text-sm font-medium">
186+
Telefone (opcional)
187+
</Label>
188+
<Input
189+
id="student-phone"
190+
value={phone}
191+
onChange={(e) => setPhone(e.target.value)}
192+
placeholder="Ex: +55 (11) 99999-9999"
193+
/>
194+
</div>
195+
180196
{/* Password */}
181197
<div className="space-y-2">
182198
<Label htmlFor="student-password" className="text-sm font-medium">

frontend/src/components/modals/students/EditStudentModal.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface EditStudentFormData {
2626
email: string;
2727
/** Empty string means keep current password */
2828
password: string;
29+
phone?: string;
2930
}
3031

3132
export function EditStudentModal({
@@ -38,6 +39,7 @@ export function EditStudentModal({
3839
}: EditStudentModalProps) {
3940
const [name, setName] = useState("");
4041
const [email, setEmail] = useState("");
42+
const [phone, setPhone] = useState("");
4143
const [password, setPassword] = useState("");
4244
const [emailExists, setEmailExists] = useState(false);
4345
const [checkingEmail, setCheckingEmail] = useState(false);
@@ -47,6 +49,7 @@ export function EditStudentModal({
4749
if (student) {
4850
setName(student.name);
4951
setEmail(student.email);
52+
setPhone(student.phone ?? "");
5053
setPassword("");
5154
setEmailExists(false);
5255
}
@@ -55,7 +58,7 @@ export function EditStudentModal({
5558
function handleSubmit(e: React.FormEvent) {
5659
e.preventDefault();
5760
if (!student) return;
58-
onSubmit({ id: student.id, name, email, password });
61+
onSubmit({ id: student.id, name, email, password, phone });
5962
}
6063

6164
const isAdminEmail = adminEmail !== "" && email.trim().toLowerCase() === adminEmail;
@@ -149,6 +152,19 @@ export function EditStudentModal({
149152
)}
150153
</div>
151154

155+
{/* Phone */}
156+
<div className="space-y-2">
157+
<Label htmlFor="edit-phone" className="text-sm font-medium">
158+
Telefone (opcional)
159+
</Label>
160+
<Input
161+
id="edit-phone"
162+
value={phone}
163+
onChange={(e) => setPhone(e.target.value)}
164+
placeholder="Ex: +55 (11) 99999-9999"
165+
/>
166+
</div>
167+
152168
{/* Password */}
153169
<div className="space-y-2">
154170
<Label htmlFor="edit-password" className="text-sm font-medium">
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import type { ReactNode } from "react";
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogHeader,
6+
DialogTitle,
7+
} from "@/components/ui/dialog";
8+
import { Badge } from "@/components/ui/badge";
9+
import { Button } from "@/components/ui/button";
10+
import type { Student } from "@/types/student";
11+
import { formatBrazilianDate } from "@/utils/formatDate";
12+
import { statusColors, statusLabels } from "@/types/student";
13+
14+
interface StudentInfoModalProps {
15+
open: boolean;
16+
onOpenChange: (open: boolean) => void;
17+
student: Student | null;
18+
}
19+
20+
export function StudentInfoModal({ open, onOpenChange, student }: StudentInfoModalProps) {
21+
if (!student) return null;
22+
23+
const extra = student.extra_data || {};
24+
const source = extra.source || "Não identificado";
25+
const paytData = extra.payt || {};
26+
const utms = paytData.utms || {};
27+
28+
// Customer details (from direct customer object, payt nested customer, or fallback fields)
29+
const customer = extra.customer || paytData.customer || {};
30+
const customerDoc = customer.doc || extra.doc || customer.cpf || customer.cnpj;
31+
const customerUrl = customer.url;
32+
const customerCode = customer.code || paytData.customer_code || extra.customer_code;
33+
34+
// Transaction details
35+
const transactionId = paytData.transaction_id || extra.transaction_id;
36+
const paymentMethod = paytData.payment_method || extra.payment_method;
37+
const sellerId = paytData.seller_id;
38+
const chatwootContact = extra.chatwoot_contact_id;
39+
const chatwootConv = extra.chatwoot_conversation_id;
40+
41+
// Filter out common keys for the additional data section
42+
const knownKeys = ["source", "full_name", "payt", "customer", "chatwoot_contact_id", "chatwoot_conversation_id", "transaction_id", "payment_method", "customer_code", "doc"];
43+
const otherKeys = Object.keys(extra).filter(key => !knownKeys.includes(key));
44+
45+
return (
46+
<Dialog open={open} onOpenChange={onOpenChange}>
47+
<DialogContent className="sm:max-w-md max-h-[85vh] overflow-y-auto p-4 gap-3">
48+
<DialogHeader className="pb-2 border-b">
49+
<DialogTitle className="flex items-center gap-1.5 text-base font-bold">
50+
<i className="ri-information-line text-primary text-lg" />
51+
Ficha do Aluno
52+
</DialogTitle>
53+
</DialogHeader>
54+
55+
<div className="space-y-4 text-xs">
56+
{/* Aluno Header */}
57+
<div className="flex items-center justify-between bg-muted/30 p-2.5 rounded-lg border">
58+
<div className="min-w-0">
59+
<h3 className="font-bold text-sm truncate text-foreground">{student.name}</h3>
60+
<p className="text-muted-foreground text-xs truncate">{student.email}</p>
61+
</div>
62+
<Badge variant="secondary" className={`text-[10px] py-0 px-1.5 font-medium ${statusColors[student.status]}`}>
63+
{statusLabels[student.status]}
64+
</Badge>
65+
</div>
66+
67+
{/* Dados Pessoais / Cadastro */}
68+
<div className="space-y-1.5">
69+
<h4 className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Dados Pessoais & Contato</h4>
70+
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 bg-muted/10 p-2.5 rounded-lg border">
71+
<DataRow label="Telefone" value={student.phone || customer.phone || "Não informado"} />
72+
<DataRow label="CPF/CNPJ" value={customerDoc || "Não informado"} />
73+
<DataRow label="Cadastro" value={formatBrazilianDate(student.createdAt)} />
74+
<DataRow
75+
label="Origem"
76+
value={
77+
<span className="capitalize font-semibold text-primary">
78+
{source}
79+
</span>
80+
}
81+
/>
82+
</div>
83+
</div>
84+
85+
{/* Dados de Venda / Checkout */}
86+
{(transactionId || customerCode || sellerId || paymentMethod || customerUrl) && (
87+
<div className="space-y-1.5">
88+
<div className="flex justify-between items-center">
89+
<h4 className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Informações da Venda</h4>
90+
{customerUrl && (
91+
<a
92+
href={customerUrl}
93+
target="_blank"
94+
rel="noopener noreferrer"
95+
className="text-[10px] text-primary hover:underline flex items-center gap-0.5"
96+
>
97+
Ver na plataforma <i className="ri-external-link-line text-[9px]" />
98+
</a>
99+
)}
100+
</div>
101+
<div className="grid grid-cols-1 gap-y-1.5 bg-muted/10 p-2.5 rounded-lg border">
102+
{transactionId && <DataRow label="ID Transação" value={transactionId} isMono />}
103+
{customerCode && <DataRow label="Cód. Cliente" value={customerCode} isMono />}
104+
{sellerId && <DataRow label="ID Seller" value={sellerId} isMono />}
105+
{paymentMethod && <DataRow label="Método Pgto" value={paymentMethod} className="capitalize" />}
106+
</div>
107+
</div>
108+
)}
109+
110+
{/* UTMs */}
111+
{Object.values(utms).some(val => val) && (
112+
<div className="space-y-1.5">
113+
<h4 className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Parâmetros de Campanha (UTM)</h4>
114+
<div className="grid grid-cols-2 gap-1 bg-muted/10 p-2 rounded-lg border">
115+
{Object.entries(utms).map(([key, val]) => {
116+
if (!val) return null;
117+
return (
118+
<div key={key} className="flex justify-between items-center py-0.5 border-b border-dashed border-muted last:border-0">
119+
<span className="text-muted-foreground text-[10px] uppercase font-mono">{key}:</span>
120+
<span className="font-semibold text-foreground truncate max-w-[120px]" title={String(val)}>
121+
{renderValue(val)}
122+
</span>
123+
</div>
124+
);
125+
})}
126+
</div>
127+
</div>
128+
)}
129+
130+
{/* Integrações */}
131+
{(chatwootContact || chatwootConv) && (
132+
<div className="space-y-1.5">
133+
<h4 className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Integrações de Chat</h4>
134+
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 bg-muted/10 p-2.5 rounded-lg border">
135+
{chatwootContact && <DataRow label="Chatwoot Contato" value={chatwootContact} isMono />}
136+
{chatwootConv && <DataRow label="Chatwoot Conversa" value={chatwootConv} isMono />}
137+
</div>
138+
</div>
139+
)}
140+
141+
{/* Dados Adicionais Raw JSON */}
142+
{otherKeys.length > 0 && (
143+
<div className="space-y-1">
144+
<h4 className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Outros Metadados</h4>
145+
<pre className="text-[10px] font-mono bg-muted/40 p-2 rounded border overflow-x-auto max-h-[100px] leading-tight">
146+
{JSON.stringify(
147+
otherKeys.reduce((acc, key) => ({ ...acc, [key]: extra[key] }), {}),
148+
null,
149+
2
150+
)}
151+
</pre>
152+
</div>
153+
)}
154+
155+
{/* Close Action */}
156+
<div className="flex justify-end pt-1">
157+
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)} className="w-full sm:w-auto h-8 text-xs">
158+
Fechar
159+
</Button>
160+
</div>
161+
</div>
162+
</DialogContent>
163+
</Dialog>
164+
);
165+
}
166+
167+
function renderValue(val: any): ReactNode {
168+
if (typeof val === "string" && (val.startsWith("http://") || val.startsWith("https://"))) {
169+
return (
170+
<a
171+
href={val}
172+
target="_blank"
173+
rel="noopener noreferrer"
174+
className="text-primary hover:underline inline-flex items-center gap-0.5 font-semibold"
175+
title={val}
176+
>
177+
Link <i className="ri-external-link-line text-[10px]" />
178+
</a>
179+
);
180+
}
181+
return val;
182+
}
183+
184+
interface DataRowProps {
185+
label: string;
186+
value: ReactNode;
187+
isMono?: boolean;
188+
className?: string;
189+
}
190+
191+
function DataRow({ label, value, isMono = false, className = "" }: DataRowProps) {
192+
return (
193+
<div className="flex justify-between items-center py-0.5 border-b border-dashed border-muted last:border-0 min-w-0">
194+
<span className="text-muted-foreground shrink-0 pr-2">{label}:</span>
195+
<span className={`font-medium text-foreground truncate max-w-[200px] ${isMono ? "font-mono text-[11px]" : ""} ${className}`} title={typeof value === 'string' ? value : undefined}>
196+
{renderValue(value)}
197+
</span>
198+
</div>
199+
);
200+
}

0 commit comments

Comments
 (0)