/* Estils per a la graella de fons */
.grid-bg {
background-image: radial-gradient(#e2e8f0 1px, transparent 1px);
background-size: 30px 30px;
}
.node-pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
}
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: #f1f1f1; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
/* WordPress Compatibility */
.wp-sociograma { line-height: 1.5; color: #1e293b; }
/* Estats de selecció */
.connecting-mode .student-node { cursor: cell !important; }
.connecting-mode .student-card { cursor: cell !important; border-color: #6366f1 !important; background-color: #eef2ff !important; }
// --- DADES I CONFIGURACIÓ ---
let students = [
{ id: '1', name: 'Marc García', role: 'agressor', profiles: ['tdah'], x: 150, y: 150 },
{ id: '2', name: 'Laia Soler', role: 'victima', profiles: ['tea'], x: 400, y: 250 },
{ id: '3', name: 'Pau Martí', role: 'defensor', profiles: [], x: 450, y: 120 },
{ id: '4', name: 'Sílvia Vila', role: 'espectador', profiles: ['immigra'], x: 200, y: 350 }
];
let relations = [
{ from: '1', to: '2', type: 'conflicte' },
{ from: '3', to: '2', type: 'amistat' }
];
const ROLES = {
agressor: { label: 'Agressor/a', color: 'bg-red-500', border: 'border-red-600', text: 'text-red-700' },
victima: { label: 'Víctima', color: 'bg-purple-500', border: 'border-purple-600', text: 'text-purple-700' },
defensor: { label: 'Defensor/a', color: 'bg-green-500', border: 'border-green-600', text: 'text-green-700' },
seguidor: { label: 'Seguidor/a', color: 'bg-orange-500', border: 'border-orange-600', text: 'text-orange-700' },
espectador: { label: 'Espectador/a', color: 'bg-blue-500', border: 'border-blue-600', text: 'text-blue-700' },
none: { label: 'Sense rol', color: 'bg-gray-400', border: 'border-gray-500', text: 'text-gray-600' }
};
const INTERVENTIONS = {
agressor: ["Establir límits clars", "Treballar l'empatia", "Mediació dirigida"],
victima: ["Pla de protecció", "Acompanyament emocional", "Grup de suport entre iguals"],
defensor: ["Reforçar la conducta positiva", "Formació en mediació", "Lideratge a l'aula"],
tea: ["Anticipació de canvis", "Suport visual", "Guia de relació social"],
tdah: ["Adaptació de tasques", "Control d'impulsos", "Espais de pausa activa"],
immigra: ["Acompanyament lingüístic", "Pla d'acollida", "Inclusió cultural"]
};
let selectedStudentId = null;
let draggingNodeId = null;
let isConnecting = false;
let connectSourceId = null;
let connectType = null;
let editModeId = null;
// --- FUNCIONS PRINCIPALS ---
function init() {
lucide.createIcons();
renderList();
renderCanvas();
setupDragAndDrop();
}
function renderList() {
const query = document.getElementById('search').value.toLowerCase();
const container = document.getElementById('student-list');
const filtered = students.filter(s => s.name.toLowerCase().includes(query));
container.innerHTML = filtered.map(s => {
const r = ROLES[s.role] || ROLES.none;
const isSource = connectSourceId === s.id;
return `
${!isConnecting ? `
` : ''}
`;
}).join('');
lucide.createIcons();
}
function renderCanvas() {
const nodeContainer = document.getElementById('nodes-layer');
const linksContainer = document.getElementById('links-group');
nodeContainer.innerHTML = '';
linksContainer.innerHTML = '';
// Dibuixar Relacions
relations.forEach(rel => {
const from = students.find(s => s.id === rel.from);
const to = students.find(s => s.id === rel.to);
if (!from || !to) return;
const color = rel.type === 'conflicte' ? '#ef4444' : (rel.type === 'amistat' ? '#22c55e' : '#94a3b8');
const marker = rel.type === 'conflicte' ? 'url(#arrowhead-conflict)' :
(rel.type === 'amistat' ? 'url(#arrowhead-friend)' : 'url(#arrowhead)');
const dash = rel.type === 'influencia' ? '5,5' : '';
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", from.x + 24);
line.setAttribute("y1", from.y + 24);
line.setAttribute("x2", to.x + 24);
line.setAttribute("y2", to.y + 24);
line.setAttribute("stroke", color);
line.setAttribute("stroke-width", "2");
line.setAttribute("stroke-dasharray", dash);
line.setAttribute("marker-end", marker);
line.setAttribute("class", "cursor-pointer hover:stroke-indigo-400 transition-colors");
line.onclick = (e) => { e.stopPropagation(); removeRelation(rel); };
linksContainer.appendChild(line);
});
// Dibuixar Nodes
students.forEach(s => {
const r = ROLES[s.role] || ROLES.none;
const isSource = connectSourceId === s.id;
const div = document.createElement('div');
div.className = `student-node absolute pointer-events-auto cursor-grab active:cursor-grabbing flex flex-col items-center group`;
div.style.left = `${s.x}px`;
div.style.top = `${s.y}px`;
div.id = `node-${s.id}`;
div.innerHTML = `
${s.profiles.length > 0 ? `
` : ''}
`;
renderList();
renderCanvas();
lucide.createIcons();
}
// --- DRAG AND DROP ---
function startDragging(e, id) {
if (isConnecting) return; // Desactivar drag mentre connectem
draggingNodeId = id;
}
function setupDragAndDrop() {
const container = document.getElementById('canvas-container');
container.onmousemove = (e) => {
if (draggingNodeId) {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left - 24;
const y = e.clientY - rect.top - 24;
const s = students.find(st => st.id === draggingNodeId);
s.x = x;
s.y = y;
renderCanvas();
}
};
window.onmouseup = () => {
draggingNodeId = null;
};
}
// --- GESTIÓ DE CONNEXIONS (FLUX MILLORAT) ---
function startConnectionFlow(id) {
connectSourceId = id;
document.getElementById('rel-modal-overlay').classList.remove('hidden');
document.getElementById('rel-modal-overlay').classList.add('flex');
}
function initiateConnection(type) {
connectType = type;
isConnecting = true;
closeRelModal();
// Mostrar feedback visual
document.getElementById('connection-banner').classList.remove('hidden');
document.getElementById('connection-banner').classList.add('flex');
document.getElementById('app').classList.add('connecting-mode');
renderList();
renderCanvas();
}
function completeConnection(targetId) {
if (targetId === connectSourceId) {
cancelConnection();
return;
}
// Evitar duplicats
const exists = relations.some(r => r.from === connectSourceId && r.to === targetId && r.type === connectType);
if (!exists) {
relations.push({ from: connectSourceId, to: targetId, type: connectType });
}
cancelConnection();
}
function cancelConnection() {
isConnecting = false;
connectSourceId = null;
connectType = null;
document.getElementById('connection-banner').classList.add('hidden');
document.getElementById('connection-banner').classList.remove('flex');
document.getElementById('app').classList.remove('connecting-mode');
renderList();
renderCanvas();
}
// --- ALTRES MODALS ---
function openAddModal() {
editModeId = null;
document.getElementById('modal-title').innerText = "Nou Alumne";
document.getElementById('form-name').value = "";
document.getElementById('form-role').value = "none";
document.querySelectorAll('input[name="profile"]').forEach(c => c.checked = false);
document.getElementById('modal-overlay').classList.remove('hidden');
document.getElementById('modal-overlay').classList.add('flex');
}
function openEditModal(id) {
editModeId = id;
const s = students.find(x => x.id === id);
document.getElementById('modal-title').innerText = "Editar Alumne";
document.getElementById('form-name').value = s.name;
document.getElementById('form-role').value = s.role;
document.querySelectorAll('input[name="profile"]').forEach(c => {
c.checked = s.profiles.includes(c.value);
});
document.getElementById('modal-overlay').classList.remove('hidden');
document.getElementById('modal-overlay').classList.add('flex');
}
function closeModal() {
document.getElementById('modal-overlay').classList.add('hidden');
document.getElementById('modal-overlay').classList.remove('flex');
}
function closeRelModal() {
document.getElementById('rel-modal-overlay').classList.add('hidden');
document.getElementById('rel-modal-overlay').classList.remove('flex');
}
function saveStudent() {
const name = document.getElementById('form-name').value;
const role = document.getElementById('form-role').value;
const profiles = Array.from(document.querySelectorAll('input[name="profile"]:checked')).map(c => c.value);
if (!name) return;
if (editModeId) {
const s = students.find(x => x.id === editModeId);
s.name = name;
s.role = role;
s.profiles = profiles;
} else {
students.push({
id: Date.now().toString(),
name, role, profiles,
x: Math.random() * 300 + 50,
y: Math.random() * 300 + 50
});
}
closeModal();
renderList();
renderCanvas();
if(selectedStudentId) selectStudent(selectedStudentId);
}
function deleteStudent(id) {
students = students.filter(s => s.id !== id);
relations = relations.filter(r => r.from !== id && r.to !== id);
closeDetails();
renderList();
renderCanvas();
}
function removeRelation(rel) {
relations = relations.filter(r => !(r.from === rel.from && r.to === rel.to && r.type === rel.type));
renderCanvas();
}
function closeDetails() {
selectedStudentId = null;
document.getElementById('details-panel').classList.add('hidden');
document.getElementById('details-panel').classList.remove('flex');
renderList();
renderCanvas();
}
function exportData() {
const data = { students, relations };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sociograma_${new Date().toLocaleDateString()}.json`;
a.click();
}
window.onload = init;
Selecciona l'alumne destí per crear la relació...
Mapa de Convivència Escolar
Eina de diagnòstic i intervenció per a l'aula
Llegenda de Rols
Agressor
Víctima
Defensor
Seguidor
Espectador
Sense Rol
Nou Alumne
Cap rol definit
⚡ Agressor/a
🛡️ Víctima
🤝 Defensor/a
👥 Seguidor/a
👁️ Espectador/a
Tipus de Relació
${s.name}
${r.label}
${s.name.charAt(0)}
${s.name}
`;
nodeContainer.appendChild(div);
});
}
// --- LOGICA D'INTERACCIÓ ---
function handleStudentClick(id) {
if (isConnecting) {
completeConnection(id);
} else {
selectStudent(id);
}
}
function selectStudent(id) {
selectedStudentId = id;
const s = students.find(x => x.id === id);
const panel = document.getElementById('details-panel');
const r = ROLES[s.role] || ROLES.none;
panel.classList.remove('hidden');
panel.classList.add('flex');
let relevant = [...(INTERVENTIONS[s.role] || [])];
s.profiles.forEach(p => relevant = [...relevant, ...(INTERVENTIONS[p] || [])]);
panel.innerHTML = `
${s.name}
${r.label}
${s.profiles.map(p => `${p}`).join('')}
${relevant.length > 0 ? relevant.map(int => `
${int}
`).join('') : 'No s\'ha determinat un pla d\'acció específic encara.
'}