Files
itflow/agent/modals/project/project.php
johnnyq bbb2456f0b Projects modernization: projects list with Active/Mine/Overdue/Due soon/Stalled/Over budget/Ready to close/Completed/Archived/All views, ticket and task progress bars, time logged against budget with unbilled hours, due badges, manager changed in place, manager/client/created filters, bulk set manager, set due date, complete, reopen, archive, restore and delete, CSV/PDF export; project page with header actions, attention banners, tickets/tasks/time/due stat cards, Tickets/Tasks/Activity tabs, scope, details and team with hours; project quick look; New/Edit Project with client contact, start date, budget hours and template that can assign its tickets to the manager; Add tickets by picking or by number; add a project template's tickets to an existing project; template ticket subjects fill in [Client], [Project], [Month], [Quarter] and [Year]; project templates admin rework with ordered ticket templates, multi-add, duplicate, archive and restore; projects on the ticket page, client overview, dashboard, calendar, global search and client actions menu; New Ticket offers internal projects; tickets only join a project of their own client or an internal one; completing a project is checked on the server and completed projects can be reopened; deleting a project keeps its tickets; deleting a client deletes its projects; bulk ticket creation limits project choices to matching clients; project template reordering limited to admins; new manager notifications; new functions/project.php; removed project_link_closed_ticket.php; DB 2.8.6 adds project_start, project_budget_hours, project_contact_id and a ticket_project_id index, and cleans up orphaned projects, ticket project links and template links
2026-10-15 16:35:00 -04:00

114 lines
5.7 KiB
PHP

<?php
/*
* Project quick look - from the projects list, a ticket that is on a
* project, the client overview: how far along it is, its time against the
* budget, when it is due and who runs it, and its open tickets. The project
* page is one click away for the rest.
*/
require_once '../../../includes/modal_header.php';
$project = projectForModal($_GET['id'] ?? 0, 1);
$project_id = intval($project['project_id']);
$project_client_id = intval($project['project_client_id']);
$is_open = projectIsOpen($project);
$stats = projectStats([$project_id])[$project_id];
$flags = projectFlags($project, $stats);
$reference = escapeHtml(projectReference($project));
$url = escapeHtml(projectUrl($project_id, $project_client_id));
$can_edit = projectCan(2);
$open_tickets = ticketRows("tickets.ticket_project_id = $project_id AND " . TICKET_OPEN_SQL . ' ' . clientScopeSql('tickets.ticket_client_id'), 8);
$description = trim((string) $project['project_description']);
ob_start();
?>
<div class="modal-header bg-dark" data-bs-theme="dark">
<div class="min-w-0">
<div class="small d-flex flex-wrap align-items-center gap-2 mb-1">
<span class="font-monospace text-body-secondary"><?= $reference ?></span>
<?= projectStatePill($project) ?>
<?= projectDuePill($project) ?>
</div>
<h5 class="modal-title fw-semibold text-break"><?= escapeHtml($project['project_name']) ?></h5>
<div class="small text-body-secondary mt-1">
<?= $project_client_id ? escapeHtml($project['client_name']) : 'Internal - no client' ?>
<span class="mx-1">&middot;</span>started <?= timeAgo($project['project_start'] ?: $project['project_created_at']) ?>
</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php foreach ($flags as $flag) { ?>
<div class="alert alert-<?= $flag['tone'] ?> py-1 px-2 small mb-2"><?= escapeHtml($flag['text']) ?></div>
<?php } ?>
<div class="row g-3 mb-3">
<div class="col-sm-6">
<div class="small text-body-secondary fw-semibold text-uppercase mb-1">Progress</div>
<?= projectProgressHtml($stats) ?>
</div>
<div class="col-sm-6">
<div class="small text-body-secondary fw-semibold text-uppercase mb-1">Time<?= floatval($project['project_budget_hours']) > 0 ? ' / budget' : '' ?></div>
<?= projectTimeHtml($project, $stats) ?>
<?php if ($stats['unbilled_seconds']) { ?>
<div class="small text-body-secondary mt-1"><?= projectHoursText($stats['unbilled_seconds']) ?> billable, not invoiced</div>
<?php } ?>
</div>
</div>
<dl class="itflow-asset-dl mb-3">
<dt>Manager</dt>
<dd><?= formatAssignee($project['project_manager'], $project['user_name'], $project['user_avatar'], false, 'No manager') ?></dd>
<?php if (intval($project['project_contact_id']) && $project['contact_name'] !== null) { ?>
<dt>Contact</dt>
<dd>
<?= contactLink($project_client_id, $project['project_contact_id'], $project['contact_name'], $project['contact_photo'] ?? '') ?>
<?php if ($project['contact_email']) { ?>
<div class="small"><a href="mailto:<?= escapeHtml($project['contact_email']) ?>"><?= escapeHtml($project['contact_email']) ?></a></div>
<?php } ?>
</dd>
<?php } ?>
<?php if ($project['project_start']) { ?>
<dt>Start</dt><dd><?= escapeHtml(date('D M j, Y', strtotime($project['project_start']))) ?></dd>
<?php } ?>
<dt>Due</dt><dd><?= $project['project_due'] ? escapeHtml(date('D M j, Y', strtotime($project['project_due']))) : '<span class="text-body-secondary">-</span>' ?></dd>
<?php if ($project['project_completed_at']) { ?>
<dt>Completed</dt><dd><?= escapeHtml(date('D M j, Y', strtotime($project['project_completed_at']))) ?></dd>
<?php } ?>
</dl>
<?php if ($description !== '') { ?>
<h6 class="small text-body-secondary fw-semibold text-uppercase">Scope</h6>
<div class="border rounded p-2 mb-3 small itflow-project-scope"><?= nl2br(escapeHtml(mb_strlen($description) > 600 ? mb_substr($description, 0, 600) . '…' : $description)) ?></div>
<?php } ?>
<h6 class="small text-body-secondary fw-semibold text-uppercase">
Open tickets<?php if ($stats['open'] > count($open_tickets)) { ?> <span class="fw-normal text-lowercase">- <?= count($open_tickets) ?> of <?= $stats['open'] ?></span><?php } ?>
</h6>
<?php if ($open_tickets) { ?>
<?= ticketTableHtml($open_tickets, ['compact' => true]) ?>
<?php } else { ?>
<div class="small text-body-secondary"><?= $stats['tickets'] ? 'None - every ticket is resolved or closed.' : 'No tickets on it yet.' ?></div>
<?php } ?>
</div>
<div class="modal-footer">
<?php if ($can_edit && $is_open) { ?>
<button type="button" class="btn btn-outline-secondary me-auto ajax-modal" data-modal-size="lg" data-modal-url="modals/ticket/ticket_add.php?<?= $project_client_id ? "client_id=$project_client_id&" : '' ?>project_id=<?= $project_id ?>"><i class="fas fa-plus me-2"></i>New ticket</button>
<?php } ?>
<?php if ($can_edit && !$project['project_archived_at']) { ?>
<button type="button" class="btn btn-light ajax-modal" data-modal-size="lg" data-modal-url="modals/project/project_edit.php?id=<?= $project_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
<?php } ?>
<a class="btn btn-primary text-bold" href="<?= $url ?>"><i class="fas fa-project-diagram me-2"></i>Open project</a>
</div>
<?php
require_once '../../../includes/modal_footer.php';