fix: corrige tela preta da camera traseira e aplica correcao global de imagens
This commit is contained in:
parent
980b15905b
commit
167c8431eb
|
|
@ -49,6 +49,20 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
|
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
|
||||||
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
|
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
|
||||||
|
|
||||||
|
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
||||||
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
if (url.startsWith('data:image') || url.startsWith('blob:')) return url;
|
||||||
|
if (url.startsWith('/storage/')) return url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (match) return `/storage/${match[1]}`;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
const toggleAttendanceStatus = (record: any) => {
|
const toggleAttendanceStatus = (record: any) => {
|
||||||
let updatedAttendance = [...(data.attendance || [])];
|
let updatedAttendance = [...(data.attendance || [])];
|
||||||
|
|
||||||
|
|
@ -404,9 +418,9 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
className="flex items-center justify-between p-4 bg-slate-50 hover:bg-indigo-50 rounded-xl border border-slate-100 hover:border-indigo-200 cursor-pointer transition-all group"
|
className="flex items-center justify-between p-4 bg-slate-50 hover:bg-indigo-50 rounded-xl border border-slate-100 hover:border-indigo-200 cursor-pointer transition-all group"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-black text-sm flex-shrink-0 overflow-hidden">
|
<div className="w-10 h-10 rounded-full bg-slate-100 overflow-hidden border border-slate-200 flex items-center justify-center text-indigo-600 font-black text-sm flex-shrink-0">
|
||||||
{student.photo ? (
|
{student.photo ? (
|
||||||
<img src={student.photo} alt={student.name} className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(student.photo)} alt={student.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
student.name.charAt(0).toUpperCase()
|
student.name.charAt(0).toUpperCase()
|
||||||
)}
|
)}
|
||||||
|
|
@ -441,11 +455,13 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
|
|
||||||
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-black overflow-hidden flex-shrink-0">
|
<div className="w-20 h-20 rounded-full bg-slate-50 border-4 border-white shadow-xl overflow-hidden flex-shrink-0">
|
||||||
{selectedStudent.photo ? (
|
{selectedStudent.photo ? (
|
||||||
<img src={selectedStudent.photo} alt={selectedStudent.name} className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(selectedStudent.photo)} alt={selectedStudent.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<User size={24} />
|
<div className="w-full h-full flex items-center justify-center bg-indigo-100 text-indigo-600 font-black text-2xl">
|
||||||
|
{selectedStudent.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -834,10 +850,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto bg-slate-200 p-4 flex items-center justify-center">
|
<div className="flex-1 overflow-auto bg-slate-200 p-4 flex items-center justify-center">
|
||||||
{viewingAttachment.startsWith('data:application/pdf') || viewingAttachment.includes('.pdf') ? (
|
{viewingAttachment.includes('pdf') ? (
|
||||||
<iframe src={viewingAttachment} className="w-full h-full min-h-[70vh] rounded-lg shadow-sm bg-white" />
|
<iframe src={normalizePhotoUrl(viewingAttachment)} className="w-full h-full min-h-[70vh] rounded-lg shadow-sm border border-slate-100" />
|
||||||
) : (
|
) : (
|
||||||
<img src={viewingAttachment} className="max-w-full max-h-full object-contain rounded-lg shadow-sm" alt="Documento" />
|
<img src={normalizePhotoUrl(viewingAttachment)} className="max-w-full max-h-full object-contain rounded-lg shadow-xl" alt="Documento" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,20 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
const [scheduleClass, setScheduleClass] = useState<Class | null>(null); // For LessonSchedule component
|
const [scheduleClass, setScheduleClass] = useState<Class | null>(null); // For LessonSchedule component
|
||||||
const [viewingStudentsClass, setViewingStudentsClass] = useState<Class | null>(null); // For student list modal
|
const [viewingStudentsClass, setViewingStudentsClass] = useState<Class | null>(null); // For student list modal
|
||||||
|
|
||||||
|
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
||||||
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
if (url.startsWith('data:image') || url.startsWith('blob:')) return url;
|
||||||
|
if (url.startsWith('/storage/')) return url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (match) return `/storage/${match[1]}`;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState<Omit<Class, 'id'>>({
|
const [formData, setFormData] = useState<Omit<Class, 'id'>>({
|
||||||
name: '',
|
name: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
|
|
@ -531,7 +545,7 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-slate-100 overflow-hidden border border-slate-200">
|
<div className="w-10 h-10 rounded-full bg-slate-100 overflow-hidden border border-slate-200">
|
||||||
{student.photo ? (
|
{student.photo ? (
|
||||||
<img src={student.photo} alt={student.name} className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(student.photo)} alt={student.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-slate-300">
|
<div className="w-full h-full flex items-center justify-center text-slate-300">
|
||||||
<User size={20} />
|
<User size={20} />
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,20 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
const periods = data.periods || [];
|
const periods = data.periods || [];
|
||||||
const grades = data.grades || [];
|
const grades = data.grades || [];
|
||||||
|
|
||||||
|
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
||||||
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
if (url.startsWith('data:image') || url.startsWith('blob:')) return url;
|
||||||
|
if (url.startsWith('/storage/')) return url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (match) return `/storage/${match[1]}`;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddSubject = () => {
|
const handleAddSubject = () => {
|
||||||
if (!newSubjectName.trim()) {
|
if (!newSubjectName.trim()) {
|
||||||
showAlert('Atenção', '⚠️ Por favor, informe o nome da disciplina.', 'warning');
|
showAlert('Atenção', '⚠️ Por favor, informe o nome da disciplina.', 'warning');
|
||||||
|
|
@ -364,7 +378,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div className="w-10 h-10 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-400 flex-shrink-0 overflow-hidden">
|
<div className="w-10 h-10 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-400 flex-shrink-0 overflow-hidden">
|
||||||
{student.photo ? (
|
{student.photo ? (
|
||||||
<img src={student.photo} alt={student.name} className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(student.photo)} alt={student.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<User size={20} />
|
<User size={20} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,20 @@ const Settings: React.FC<SettingsProps> = ({ data, updateData, setData }) => {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'perfil' | 'monitoramento'>('perfil');
|
const [activeTab, setActiveTab] = useState<'perfil' | 'monitoramento'>('perfil');
|
||||||
const [apiLogs, setApiLogs] = useState<any[]>([]);
|
const [apiLogs, setApiLogs] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
||||||
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
if (url.startsWith('data:image')) return url;
|
||||||
|
if (url.startsWith('/storage/')) return url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (match) return `/storage/${match[1]}`;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
const [systemStats, setSystemStats] = useState<any>(null);
|
const [systemStats, setSystemStats] = useState<any>(null);
|
||||||
|
|
||||||
const fetchStats = () => {
|
const fetchStats = () => {
|
||||||
|
|
@ -334,7 +348,9 @@ const Settings: React.FC<SettingsProps> = ({ data, updateData, setData }) => {
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div className="w-40 h-40 rounded-xl bg-slate-50 border-2 border-dashed border-slate-200 flex items-center justify-center overflow-hidden relative group shadow-inner">
|
<div className="w-40 h-40 rounded-xl bg-slate-50 border-2 border-dashed border-slate-200 flex items-center justify-center overflow-hidden relative group shadow-inner">
|
||||||
{globalLogo ? (
|
{globalLogo ? (
|
||||||
<img src={globalLogo} alt="Logo" className="w-full h-full object-contain p-2" />
|
<div className="w-full h-full bg-slate-50 flex items-center justify-center p-4">
|
||||||
|
<img src={normalizePhotoUrl(globalLogo)} alt="Logo" className="w-full h-full object-contain p-2" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-slate-300 text-center p-4">
|
<div className="text-slate-300 text-center p-4">
|
||||||
<Camera size={40} className="mx-auto mb-2 opacity-20" />
|
<Camera size={40} className="mx-auto mb-2 opacity-20" />
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,21 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
||||||
}
|
}
|
||||||
}, [deepLinkStudentId, deepLinkClassId, data.students]);
|
}, [deepLinkStudentId, deepLinkClassId, data.students]);
|
||||||
|
|
||||||
|
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
||||||
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
if (url.startsWith('data:image')) return url; // Base64
|
||||||
|
if (url.startsWith('/storage/')) return url; // Já formatada
|
||||||
|
|
||||||
|
// Converte URLs absolutas (ex: https://storageedu.../alunos/file) para proxy relativo (/storage/alunos/file)
|
||||||
|
try {
|
||||||
|
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
|
||||||
|
if (match) return `/storage/${match[1]}`;
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
// Load Models
|
// Load Models
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
|
|
@ -516,6 +531,9 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
||||||
});
|
});
|
||||||
|
|
||||||
streamRef.current = stream;
|
streamRef.current = stream;
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.srcObject = stream;
|
||||||
|
}
|
||||||
setCameraActive(true);
|
setCameraActive(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error accessing camera:", err);
|
console.error("Error accessing camera:", err);
|
||||||
|
|
@ -1208,7 +1226,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-slate-100 overflow-hidden border border-slate-200">
|
<div className="w-10 h-10 rounded-full bg-slate-100 overflow-hidden border border-slate-200">
|
||||||
{student.photo ? (
|
{student.photo ? (
|
||||||
<img src={student.photo} alt={student.name} className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(student.photo)} alt={student.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-slate-400">
|
<div className="w-full h-full flex items-center justify-center text-slate-400">
|
||||||
<User size={20} />
|
<User size={20} />
|
||||||
|
|
@ -1358,7 +1376,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
||||||
</>
|
</>
|
||||||
) : formData.photo ? (
|
) : formData.photo ? (
|
||||||
<>
|
<>
|
||||||
<img src={formData.photo} alt="Student" className="w-full h-full object-cover" />
|
<img src={normalizePhotoUrl(formData.photo)} alt="Student" className="w-full h-full object-cover" />
|
||||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||||
<p className="text-white font-bold text-xs">Alterar Foto</p>
|
<p className="text-white font-bold text-xs">Alterar Foto</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,11 @@ const lockCache = new Set();
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Proxy de Imagens do MinIO (acesso público via backend)
|
// Proxy de Imagens do MinIO (acesso público via backend)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
app.get('/storage/:bucket/:key', async (req, res) => {
|
app.get('/storage/:bucket/*', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { bucket, key } = req.params;
|
const bucket = req.params.bucket;
|
||||||
|
const key = req.params[0]; // Captura tudo que vem após o bucket (incluindo barras)
|
||||||
|
|
||||||
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||||
const data = await s3Client.send(command);
|
const data = await s3Client.send(command);
|
||||||
|
|
||||||
|
|
@ -62,6 +64,7 @@ app.get('/storage/:bucket/:key', async (req, res) => {
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
data.Body.pipe(res);
|
data.Body.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(`[Storage Proxy] Erro ao buscar: ${req.params.bucket}/${req.params[0]}`, e.message);
|
||||||
res.status(404).send('Arquivo não encontrado');
|
res.status(404).send('Arquivo não encontrado');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -179,25 +182,32 @@ app.put('/api/school-data', async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/system-stats', async (req, res) => {
|
app.get('/api/system-stats', async (req, res) => {
|
||||||
|
let postgresStats = { dbSize: 'N/A', tableCount: '0' };
|
||||||
try {
|
try {
|
||||||
const dbResult = await pool.query(`
|
const dbResult = await pool.query(`
|
||||||
SELECT pg_size_pretty(pg_database_size(current_database())) as db_size,
|
SELECT pg_size_pretty(pg_database_size(current_database())) as db_size,
|
||||||
(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public') as table_count
|
(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public') as table_count
|
||||||
`);
|
`);
|
||||||
|
postgresStats = {
|
||||||
const minioStats = await getMinioStats();
|
dbSize: dbResult.rows[0].db_size,
|
||||||
|
tableCount: dbResult.rows[0].table_count
|
||||||
res.json({
|
};
|
||||||
postgres: {
|
|
||||||
dbSize: dbResult.rows[0].db_size,
|
|
||||||
tableCount: dbResult.rows[0].table_count
|
|
||||||
},
|
|
||||||
minio: minioStats
|
|
||||||
});
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('System Stats Error:', e);
|
console.error('System Stats (Postgres) Error:', e);
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let minioStats = { error: true, message: 'Not initialized' };
|
||||||
|
try {
|
||||||
|
minioStats = await getMinioStats();
|
||||||
|
} catch(e) {
|
||||||
|
console.error('System Stats (MinIO) Error:', e);
|
||||||
|
minioStats = { error: true, message: e.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
postgres: postgresStats,
|
||||||
|
minio: minioStats
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import { pool } from './services/database.js';
|
||||||
|
pool.query("SELECT pg_size_pretty(pg_database_size(current_database())) as db_size").then(console.log).catch(console.log).finally(() => pool.end());
|
||||||
Loading…
Reference in New Issue