Initial commit for consolidated dispatch time and service reports #99

This commit is contained in:
Kendrick Chan 2018-05-08 03:56:20 +08:00
parent 1c868d15c8
commit 2029781d8c
4 changed files with 709 additions and 0 deletions

View file

@ -0,0 +1,439 @@
<?php
namespace App\Controller;
use App\Ramcar\BaseController;
use App\Entity\Hub;
use App\Entity\JobOrder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use DateTime;
use DateInterval;
use DatePeriod;
class ReportController extends BaseController
{
public function dispatchTimeForm(Request $req)
{
$this->denyAccessUnlessGranted('report.view', null, 'No access.');
$params = $this->initParameters('report_dispatch_time');
return $this->render('report/dispatch-time.html.twig', $params);
}
public function dispatchTimeSubmit(Request $req)
{
// initialize error list
$error_array = [];
$start_date = $req->request->get('start_date');
$end_date = $req->request->get('end_date');
// validate dates
$dates = $this->validateDateRange($start_date, $end_date, $error_array);
// check if any errors were found
if (!empty($error_array))
{
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
else
{
// return response
return $this->json([
'success' => true,
'url' => $this->generateUrl('report_dispatch_time_generate', [
'start_date' => $dates['start'],
'end_date' => $dates['end']
])
]);
}
}
public function dispatchTimeGenerate(Request $req, $start_date, $end_date)
{
// get dates
$date_format = "Y-m-d";
$sd = DateTime::createFromFormat($date_format, $start_date);
$ed = DateTime::createFromFormat($date_format, $end_date);
// format filename
$filename = "consolidated-dispatch-time-" . $sd->format('Ymd') . '-' . $ed->format('Ymd') . ".csv";
// build response
$response = new StreamedResponse();
$response->setCallback(function() use ($sd, $ed) {
$handle = fopen('php://output', 'w+');
// make table headers
$headers = ['Agent'];
// add date interval headers
$period = $this->generateDateHeaders($sd, $ed, $headers);
$headers[] = 'Grand Total';
// set table headers
fputcsv($handle, $headers);
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
// get all users
$sql = "SELECT * FROM user ORDER BY first_name ASC";
$stmt = $conn->prepare($sql);
$stmt->execute();
$users = $stmt->fetchAll();
// initiate
$tallies = [];
$jo_counts = [];
$averages = [];
$col_jo_counts = [];
$col_totals = [];
// get all job orders
$sql = "SELECT * FROM job_order WHERE date_schedule IS NOT NULL AND date_assign IS NOT NULL AND date_assign BETWEEN ? AND ?";
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, $sd->format("Y-m-d"));
$stmt->bindValue(2, $ed->format("Y-m-d"));
$stmt->execute();
$job_orders = $stmt->fetchAll();
foreach ($job_orders as $job_order)
{
$date_schedule = DateTime::createFromFormat("Y-m-d H:i:s", $job_order['date_schedule']);
$date_assign = DateTime::createFromFormat("Y-m-d H:i:s", $job_order['date_assign']);
$date_assign_formatted = $date_assign->format("Y-m-d");
// get difference between times
$date_schedule_ts = $date_schedule->getTimestamp();
$date_assign_ts = $date_assign->getTimestamp();
$diff_mins = ($date_assign_ts - $date_schedule_ts) / 60;
// add to minutes tally
if (!isset($tallies[$job_order['assign_user_id']][$date_assign_formatted]))
$tallies[$job_order['assign_user_id']][$date_assign_formatted] = $diff_mins;
else
$tallies[$job_order['assign_user_id']][$date_assign_formatted] += $diff_mins;
// append to jo counter
if (!isset($jo_counts[$job_order['assign_user_id']][$date_assign_formatted]))
$jo_counts[$job_order['assign_user_id']][$date_assign_formatted] = 1;
else
$jo_counts[$job_order['assign_user_id']][$date_assign_formatted]++;
}
// append to csv
foreach ($users as $user)
{
// check if this user is an agent
$sql = "SELECT * FROM user_role WHERE user_id = ? AND (role_id = ? OR role_id = ?)";
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, $user['id']);
$stmt->bindValue(2, "ROLE_AGENT_TIER_1");
$stmt->bindValue(3, "ROLE_AGENT_TIER_2");
$stmt->execute();
$roles = $stmt->fetchAll();
// only include if user is agent
if (count($roles) > 0)
{
$total_mins = 0;
$total_jos = 0;
$full_name = implode(" ", [
$user['first_name'],
$user['last_name']
]);
$rowdata = [$full_name];
foreach ($period as $date)
{
$date_formatted = $date->format("Y-m-d");
// get average per date for this user
if (isset($tallies[$user['id']][$date_formatted]) && isset($jo_counts[$user['id']][$date_formatted]))
{
$date_tally = $tallies[$user['id']][$date_formatted];
$jo_tally = $jo_counts[$user['id']][$date_formatted];
$rowdata[] = $date_tally / $jo_tally;
}
else
{
$date_tally = 0;
$jo_tally = 0;
$rowdata[] = 0;
}
// add to totals
$total_mins += $date_tally;
$total_jos += $jo_tally;
// add to column totals
if (!isset($col_totals[$date_formatted]))
$col_totals[$date_formatted] = $date_tally;
else
$col_totals[$date_formatted] += $date_tally;
// append to column jo counter
if (!isset($col_jo_counts[$date_formatted]))
$col_jo_counts[$date_formatted] = $jo_tally;
else
$col_jo_counts[$date_formatted] += $jo_tally;
}
// add total to rowdata
$rowdata[] = $total_jos > 0 ? $total_mins / $total_jos : 0;
fputcsv($handle, $rowdata);
}
}
// include column totals row
$totals_row = ['GRAND TOTAL'];
foreach ($period as $date)
{
$date_formatted = $date->format("Y-m-d");
$totals_row[] = $col_jo_counts[$date_formatted] > 0 ? $col_totals[$date_formatted] / $col_jo_counts[$date_formatted] : 0;
}
// add overall total
$overall_jo_total = count($job_orders);
$totals_row[] = $overall_jo_total > 0 ? array_sum($col_totals) / $overall_jo_total : 0;
fputcsv($handle, $totals_row);
fclose($handle);
});
// add response headers
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="' . $filename);
// send
$response->send();
}
public function serviceForm(Request $req)
{
$this->denyAccessUnlessGranted('report.view', null, 'No access.');
$params = $this->initParameters('report_service');
return $this->render('report/service.html.twig', $params);
}
public function serviceSubmit(Request $req)
{
// initialize error list
$error_array = [];
$start_date = $req->request->get('start_date');
$end_date = $req->request->get('end_date');
// validate dates
$dates = $this->validateDateRange($start_date, $end_date, $error_array);
// check if any errors were found
if (!empty($error_array))
{
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
else
{
// return response
return $this->json([
'success' => true,
'url' => $this->generateUrl('report_service_generate', [
'start_date' => $dates['start'],
'end_date' => $dates['end']
])
]);
}
}
public function serviceGenerate(Request $req, $start_date, $end_date)
{
// get dates
$date_format = "Y-m-d";
$sd = DateTime::createFromFormat($date_format, $start_date);
$ed = DateTime::createFromFormat($date_format, $end_date);
// format filename
$filename = "service-" . $sd->format('Ymd') . '-' . $ed->format('Ymd') . ".csv";
// build response
$response = new StreamedResponse();
$response->setCallback(function() use ($sd, $ed) {
$handle = fopen('php://output', 'w+');
// make table headers
$headers = [
'Hub',
'Branch'
];
// add date interval headers
$period = $this->generateDateHeaders($sd, $ed, $headers);
$headers[] = 'GRAND TOTAL';
// set table headers
fputcsv($handle, $headers);
$em = $this->getDoctrine()->getManager();
$conn = $em->getConnection();
// get all hubs
$sql = "SELECT * FROM hub ORDER BY name ASC";
$stmt = $conn->prepare($sql);
$stmt->execute();
$hubs = $stmt->fetchAll();
$tallies = [];
$col_totals = [];
// get all job orders
$sql = "SELECT * FROM job_order WHERE date_schedule BETWEEN ? AND ?";
$stmt = $conn->prepare($sql);
$stmt->bindValue(1, $sd->format("Y-m-d"));
$stmt->bindValue(2, $ed->format("Y-m-d"));
$stmt->execute();
$job_orders = $stmt->fetchAll();
foreach ($job_orders as $job_order)
{
$date_sched = DateTime::createFromFormat("Y-m-d H:i:s", $job_order['date_schedule']);
$date_sched_formatted = $date_sched->format("Y-m-d");
if (!isset($tallies[$job_order['hub_id']][$date_sched_formatted]))
$tallies[$job_order['hub_id']][$date_sched_formatted] = 1;
else
$tallies[$job_order['hub_id']][$date_sched_formatted]++;
}
// append to csv
foreach ($hubs as $hub)
{
$total = 0;
$rowdata = [
$hub['name'],
$hub['branch']
];
foreach ($period as $date)
{
$date_formatted = $date->format("Y-m-d");
$date_tally = $tallies[$hub['id']][$date_formatted] ?? 0;
$rowdata[] = $date_tally;
$total += $date_tally;
// add to column totals
if (!isset($col_totals[$date_formatted]))
$col_totals[$date_formatted] = $date_tally;
else
$col_totals[$date_formatted] += $date_tally;
}
// add total to rowdata
$rowdata[] = $total;
fputcsv($handle, $rowdata);
}
// include column totals row
$totals_row = ['GRAND TOTAL', ''];
foreach ($period as $date)
$totals_row[] = $col_totals[$date->format("Y-m-d")];
// add overall total
$totals_row[] = array_sum($col_totals);
fputcsv($handle, $totals_row);
fclose($handle);
});
// add response headers
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="' . $filename);
// send
$response->send();
}
protected function validateDateRange($start_date, $end_date, &$error_array)
{
$date_format = "d M Y";
$error = false;
if (empty($start_date))
{
$error = true;
$error_array['start_date'] = 'No start date specified.';
}
else
{
$sd = DateTime::createFromFormat($date_format, $start_date);
if (!$sd || $sd->format($date_format) != $start_date)
{
$error = true;
$error_array['start_date'] = 'Invalid start date specified.';
}
}
if (empty($end_date))
{
$error = true;
$error_array['end_date'] = 'No end date specified.';
}
else
{
$ed = DateTime::createFromFormat($date_format, $end_date);
if (!$ed || $ed->format($date_format) != $end_date)
{
$error = true;
$error_array['end_date'] = 'Invalid end date specified.';
}
}
return $error ? false : [
'start' => $sd->format("Y-m-d"),
'end' => $ed->format("Y-m-d")
];
}
protected function generateDateHeaders($start_date, $end_date, &$headers)
{
$interval = DateInterval::createFromDateString('1 day');
$period = new DatePeriod($start_date, $interval, $end_date);
foreach ($period as $date)
$headers[] = $date->format('M j, Y');
return $period;
}
}

View file

@ -0,0 +1,125 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Reports</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-user"></i>
</span>
<h3 class="m-portlet__head-text">
Consolidated Dispatch Time
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right" method="post" action="{{ url('report_dispatch_time_submit') }}">
{% include 'report/range-picker.html.twig' %}
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Download Report</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
// date range picker
$('#daterange_picker').datepicker({
todayHighlight: true,
autoclose: true,
bootcssVer: 3,
format: "dd M yyyy",
orientation: "bottom left",
templates: {
leftArrow: '<i class="la la-angle-left"></i>',
rightArrow: '<i class="la la-angle-right"></i>'
}
});
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
window.location.href = response.url;
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "'], [data-name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
//firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,20 @@
<div class="m-portlet__body">
<div class="m-form__section m-form__section--first">
<div class="m-form__heading">
<h3 class="m-form__heading-title">
Date Range
</h3>
</div>
<div class="form-group m-form__group">
<div class="input-daterange input-group" id="daterange_picker">
<input type="text" class="form-control m-input" name="start_date" placeholder="Start date">
<span class="input-group-addon">
to
</span>
<input type="text" class="form-control" name="end_date" placeholder="End date">
</div>
<div class="form-control-feedback hide" data-field="start_date"></div>
<div class="form-control-feedback hide" data-field="end_date"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,125 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Reports</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-user"></i>
</span>
<h3 class="m-portlet__head-text">
Service
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right" method="post" action="{{ url('report_service_submit') }}">
{% include 'report/range-picker.html.twig' %}
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Download Report</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
// date range picker
$('#daterange_picker').datepicker({
todayHighlight: true,
autoclose: true,
bootcssVer: 3,
format: "dd M yyyy",
orientation: "bottom left",
templates: {
leftArrow: '<i class="la la-angle-left"></i>',
rightArrow: '<i class="la la-angle-right"></i>'
}
});
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
window.location.href = response.url;
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "'], [data-name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
//firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}