Add sub-project functionality with 3-level hierarchy support

Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-31 19:47:15 +00:00
parent d057d29e5b
commit c4edc6f436

View File

@@ -55,7 +55,22 @@ function save_project($proyectos_dir, $project_id, $data) {
return $result; return $result;
} }
function list_projects($proyectos_dir) { function get_project_breadcrumb($proyectos_dir, $project_id) {
$breadcrumb = [];
$current_id = $project_id;
while ($current_id) {
$project = load_project($proyectos_dir, $current_id);
if (!$project) break;
array_unshift($breadcrumb, $project);
$current_id = $project["parent_id"] ?? null;
}
return $breadcrumb;
}
function list_projects($proyectos_dir, $parent_id = null) {
$projects = []; $projects = [];
if (!is_dir($proyectos_dir)) { if (!is_dir($proyectos_dir)) {
return $projects; return $projects;
@@ -64,7 +79,15 @@ function list_projects($proyectos_dir) {
foreach ($files as $file) { foreach ($files as $file) {
$data = json_decode(file_get_contents($file), true); $data = json_decode(file_get_contents($file), true);
if ($data) { if ($data) {
$projects[] = $data; // Filter by parent_id
$project_parent = $data["parent_id"] ?? null;
if ($parent_id === null && $project_parent === null) {
// Root level projects (no parent)
$projects[] = $data;
} elseif ($parent_id !== null && $project_parent === $parent_id) {
// Sub-projects of specified parent
$projects[] = $data;
}
} else { } else {
error_log("Failed to decode JSON from file: $file"); error_log("Failed to decode JSON from file: $file");
} }
@@ -86,28 +109,62 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if ($action === "create_project") { if ($action === "create_project") {
$name = trim($_POST["name"] ?? ""); $name = trim($_POST["name"] ?? "");
$description = trim($_POST["description"] ?? ""); $description = trim($_POST["description"] ?? "");
$parent_id = trim($_POST["parent_id"] ?? "");
if ($name !== "") { if ($name !== "") {
$project_id = generate_id($name); // Determine level based on parent
$project_data = [ $level = 1;
"id" => $project_id, if ($parent_id !== "") {
"name" => $name, $parent = load_project($proyectos_dir, $parent_id);
"description" => $description, if ($parent) {
"created_at" => time(), $level = ($parent["level"] ?? 1) + 1;
"updated_at" => time(), // Enforce max 3 levels
"items" => [] if ($level > 3) {
]; $error = "No se pueden crear más de 3 niveles de sub-proyectos.";
}
save_project($proyectos_dir, $project_id, $project_data); } else {
$error = "Proyecto padre no encontrado.";
// Create project directory }
$project_dir = "$proyectos_dir/$project_id";
if (!is_dir($project_dir)) {
mkdir($project_dir, 0755, true);
} }
header("Location: /entreaulas/proyectos.php?aulario=" . urlencode($aulario_id) . "&project=" . urlencode($project_id)); if (empty($error)) {
exit; $project_id = generate_id($name);
$project_data = [
"id" => $project_id,
"name" => $name,
"description" => $description,
"created_at" => time(),
"updated_at" => time(),
"items" => [],
"subprojects" => [],
"parent_id" => $parent_id !== "" ? $parent_id : null,
"level" => $level
];
save_project($proyectos_dir, $project_id, $project_data);
// Create project directory
$project_dir = "$proyectos_dir/$project_id";
if (!is_dir($project_dir)) {
mkdir($project_dir, 0755, true);
}
// Update parent's subprojects list
if ($parent_id !== "") {
$parent = load_project($proyectos_dir, $parent_id);
if ($parent) {
if (!isset($parent["subprojects"])) {
$parent["subprojects"] = [];
}
$parent["subprojects"][] = $project_id;
$parent["updated_at"] = time();
save_project($proyectos_dir, $parent_id, $parent);
}
}
header("Location: /entreaulas/proyectos.php?aulario=" . urlencode($aulario_id) . "&project=" . urlencode($project_id));
exit;
}
} else { } else {
$error = "El nombre del proyecto es obligatorio."; $error = "El nombre del proyecto es obligatorio.";
} }
@@ -118,6 +175,21 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
if ($project_id !== "") { if ($project_id !== "") {
$project_file = "$proyectos_dir/$project_id.json"; $project_file = "$proyectos_dir/$project_id.json";
if (file_exists($project_file)) { if (file_exists($project_file)) {
// Load project to get parent_id
$project = load_project($proyectos_dir, $project_id);
// Remove from parent's subprojects list
if ($project && !empty($project["parent_id"])) {
$parent = load_project($proyectos_dir, $project["parent_id"]);
if ($parent && isset($parent["subprojects"])) {
$parent["subprojects"] = array_values(array_filter($parent["subprojects"], function($id) use ($project_id) {
return $id !== $project_id;
}));
$parent["updated_at"] = time();
save_project($proyectos_dir, $project["parent_id"], $parent);
}
}
unlink($project_file); unlink($project_file);
// Also delete project directory // Also delete project directory
$project_dir = "$proyectos_dir/$project_id"; $project_dir = "$proyectos_dir/$project_id";
@@ -305,6 +377,9 @@ $view = $current_project ? "project" : "list";
<p class="card-text"> <p class="card-text">
<small class="text-muted"> <small class="text-muted">
<?= count($project["items"] ?? []) ?> elementos <?= count($project["items"] ?? []) ?> elementos
<?php if (!empty($project["subprojects"])): ?>
· <?= count($project["subprojects"]) ?> sub-proyectos
<?php endif; ?>
</small> </small>
</p> </p>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@@ -375,34 +450,134 @@ $view = $current_project ? "project" : "list";
require_once "_incl/post-body.php"; require_once "_incl/post-body.php";
exit; exit;
endif; endif;
// Get breadcrumb path
$breadcrumb = get_project_breadcrumb($proyectos_dir, $current_project);
$project_level = $project["level"] ?? 1;
?> ?>
<!-- Breadcrumb Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>">Proyectos</a>
</li>
<?php foreach ($breadcrumb as $idx => $crumb): ?>
<?php if ($idx < count($breadcrumb) - 1): ?>
<li class="breadcrumb-item">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($crumb["id"]) ?>">
<?= htmlspecialchars($crumb["name"]) ?>
</a>
</li>
<?php else: ?>
<li class="breadcrumb-item active" aria-current="page">
<?= htmlspecialchars($crumb["name"]) ?>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ol>
</nav>
<div class="card pad"> <div class="card pad">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
<h1 class="card-title"> <h1 class="card-title">
<img src="/static/arasaac/carpeta.png" height="40" style="vertical-align: middle; background: white; padding: 5px; border-radius: 10px;"> <img src="/static/arasaac/carpeta.png" height="40" style="vertical-align: middle; background: white; padding: 5px; border-radius: 10px;">
<?= htmlspecialchars($project["name"]) ?> <?= htmlspecialchars($project["name"]) ?>
<span class="badge bg-secondary">Nivel <?= $project_level ?></span>
</h1> </h1>
<?php if (!empty($project["description"])): ?> <?php if (!empty($project["description"])): ?>
<p><?= htmlspecialchars($project["description"]) ?></p> <p><?= htmlspecialchars($project["description"]) ?></p>
<?php endif; ?> <?php endif; ?>
</div> </div>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-secondary"> <?php if (!empty($project["parent_id"])): ?>
← Volver a Proyectos <a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($project["parent_id"]) ?>" class="btn btn-secondary">
</a> ← Volver al Proyecto Padre
</a>
<?php else: ?>
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>" class="btn btn-secondary">
← Volver a Proyectos
</a>
<?php endif; ?>
</div> </div>
</div> </div>
<!-- Add Item Button --> <!-- Action Buttons -->
<div class="card pad"> <div class="card pad">
<button type="button" class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#addItemModal"> <div class="d-flex gap-2 flex-wrap">
<img src="/static/iconexperience/add.png" height="30" style="vertical-align: middle;"> <button type="button" class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#addItemModal">
Añadir Enlace o Archivo <img src="/static/iconexperience/add.png" height="30" style="vertical-align: middle;">
</button> Añadir Enlace o Archivo
</button>
<?php if ($project_level < 3): ?>
<button type="button" class="btn btn-info btn-lg" data-bs-toggle="modal" data-bs-target="#createSubProjectModal">
<img src="/static/iconexperience/add.png" height="30" style="vertical-align: middle;">
Crear Sub-Proyecto
</button>
<?php endif; ?>
</div>
</div> </div>
<!-- Sub-Projects Section -->
<?php
$subprojects = list_projects($proyectos_dir, $current_project);
if (count($subprojects) > 0):
?>
<div class="card pad">
<h3>
<img src="/static/arasaac/carpeta.png" height="25" style="vertical-align: middle; background: white; padding: 3px; border-radius: 5px;">
Sub-Proyectos
</h3>
</div>
<div id="grid-subprojects">
<?php foreach ($subprojects as $subproject): ?>
<div class="card grid-item" style="width: 300px;">
<div class="card-body">
<h5 class="card-title">
<img src="/static/arasaac/carpeta.png" height="25" style="vertical-align: middle; background: white; padding: 3px; border-radius: 5px;">
<?= htmlspecialchars($subproject["name"]) ?>
<span class="badge bg-info">Nivel <?= $subproject["level"] ?? 2 ?></span>
</h5>
<?php if (!empty($subproject["description"])): ?>
<p class="card-text"><?= htmlspecialchars($subproject["description"]) ?></p>
<?php endif; ?>
<p class="card-text">
<small class="text-muted">
<?= count($subproject["items"] ?? []) ?> elementos
<?php if (!empty($subproject["subprojects"])): ?>
· <?= count($subproject["subprojects"]) ?> sub-proyectos
<?php endif; ?>
</small>
</p>
<div class="d-flex gap-2">
<a href="/entreaulas/proyectos.php?aulario=<?= urlencode($aulario_id) ?>&project=<?= urlencode($subproject["id"]) ?>" class="btn btn-primary">
<img src="/static/iconexperience/find.png" height="20" style="vertical-align: middle;">
Abrir
</a>
<form method="post" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que quieres eliminar este sub-proyecto?');">
<input type="hidden" name="action" value="delete_project">
<input type="hidden" name="project_id" value="<?= htmlspecialchars($subproject["id"]) ?>">
<button type="submit" class="btn btn-danger">
<img src="/static/iconexperience/garbage.png" height="20" style="vertical-align: middle;">
Eliminar
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Items List --> <!-- Items List -->
<?php if (count($subprojects) > 0): ?>
<div class="card pad">
<h3>
<img src="/static/arasaac/documento.png" height="25" style="vertical-align: middle; background: white; padding: 3px; border-radius: 5px;">
Archivos y Enlaces
</h3>
</div>
<?php endif; ?>
<?php <?php
$items = $project["items"] ?? []; $items = $project["items"] ?? [];
if (count($items) > 0): if (count($items) > 0):
@@ -509,6 +684,39 @@ $view = $current_project ? "project" : "list";
</div> </div>
</div> </div>
<!-- Create Sub-Project Modal -->
<div class="modal fade" id="createSubProjectModal" tabindex="-1" aria-labelledby="createSubProjectModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createSubProjectModalLabel">Crear Sub-Proyecto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post">
<div class="modal-body">
<input type="hidden" name="action" value="create_project">
<input type="hidden" name="parent_id" value="<?= htmlspecialchars($current_project) ?>">
<div class="alert alert-info">
Este sub-proyecto se creará dentro de <strong><?= htmlspecialchars($project["name"]) ?></strong> (Nivel <?= $project_level ?>)
</div>
<div class="mb-3">
<label for="subproject_name" class="form-label">Nombre del Sub-Proyecto *</label>
<input type="text" class="form-control form-control-lg" id="subproject_name" name="name" required>
</div>
<div class="mb-3">
<label for="subproject_description" class="form-label">Descripción</label>
<textarea class="form-control" id="subproject_description" name="description" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-info">Crear Sub-Proyecto</button>
</div>
</form>
</div>
</div>
</div>
<script> <script>
document.getElementById('item_type').addEventListener('change', function() { document.getElementById('item_type').addEventListener('change', function() {
var type = this.value; var type = this.value;
@@ -544,19 +752,32 @@ $view = $current_project ? "project" : "list";
.modal-lg { .modal-lg {
max-width: 800px; max-width: 800px;
} }
.breadcrumb {
background-color: #f8f9fa;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 15px;
}
</style> </style>
<script> <script>
var msnry = new Masonry('#grid', { // Initialize Masonry for main grid
"columnWidth": 280, var grids = ['#grid', '#grid-subprojects'];
"itemSelector": ".grid-item", grids.forEach(function(gridId) {
"gutter": 10, var gridElement = document.querySelector(gridId);
"transitionDuration": 0 if (gridElement) {
var msnry = new Masonry(gridId, {
"columnWidth": 280,
"itemSelector": ".grid-item",
"gutter": 10,
"transitionDuration": 0
});
setTimeout(() => {msnry.layout()}, 150);
window.addEventListener('resize', function(event) {
msnry.layout()
}, true);
}
}); });
setTimeout(() => {msnry.layout()}, 150);
window.addEventListener('resize', function(event) {
msnry.layout()
}, true);
</script> </script>
<?php require_once "_incl/post-body.php"; ?> <?php require_once "_incl/post-body.php"; ?>