php8
[web.mtrack] / MTrack / Milestoneold.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3  
4
5 class MTrack_Milestone {
6   public $mid = null;
7   public $pmid = null;
8   public $name = null;
9   public $description = null;
10   public $duedate = null;
11   public $startdate = null;
12   public $deleted = null;
13   public $completed = null;
14   public $created = null;
15
16   static function loadByName($name)
17   {
18     foreach (MTrackDB::q('select mid from milestones where lower(name) = lower(?)', $name)
19         ->fetchAll() as $row) {
20       return new self($row[0]);
21     }
22     return null;
23   }
24
25   static function loadByID($id)
26   {
27     foreach (MTrackDB::q('select mid from milestones where mid = ?', $id)
28         ->fetchAll() as $row) {
29       return new self($row[0]);
30     }
31     return null;
32   }
33
34   static function enumMilestones($all = false)
35   {
36     if ($all) {
37       $q = MTrackDB::q('select mid, name from milestones where deleted != 1');
38     } else {
39       $q = MTrackDB::q('select mid, name from milestones where completed is null and deleted != 1');
40     }
41     $res = array();
42     foreach ($q->fetchAll(PDO::FETCH_NUM) as $row) {
43       $res[$row[0]] = $row[1];
44     }
45     return $res;
46   }
47
48   function __construct($id = null)
49   {
50     if ($id !== null) {
51       $this->mid = $id;
52
53       list($row) = MTrackDB::q('select * from milestones where mid = ?', $id)
54         ->fetchAll(PDO::FETCH_ASSOC);
55       foreach ($row as $k => $v) {
56         $this->$k = $v;
57       }
58     }
59     $this->deleted = false;
60   }
61
62   function save(MTrackChangeset $CS)
63   {
64     $this->updated = $CS->cid;
65
66     if ($this->mid === null) {
67       $this->created = $CS->cid;
68
69       MTrackDB::q('insert into milestones
70           (name, description, startdate, duedate, completed, created,
71             pmid, updated, deleted)
72           values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
73         $this->name,
74         $this->description,
75         $this->startdate,
76         $this->duedate,
77         $this->completed,
78         $this->created,
79         $this->pmid,
80         $this->updated,
81         (int)$this->deleted);
82
83       $this->mid = MTrackDB::lastInsertId('milestones', 'mid');
84     } else {
85       list($old) = MTrackDB::q(
86           'select * from milestones where mid = ?', $this->mid)->fetchAll();
87       foreach ($old as $k => $v) {
88         if ($k == 'mid' || $k == 'created' || $k == 'updated') {
89           continue;
90         }
91         $CS->add("milestone:$this->mid:$k", $v, $this->$k);
92       }
93       MTrackDB::q('update milestones set name = ?,
94           description = ?, startdate = ?, duedate = ?, completed = ?,
95           updated = ?, deleted = ?, pmid = ?
96           WHERE mid = ?',
97         $this->name,
98         $this->description,
99         $this->startdate,
100         $this->duedate,
101         $this->completed,
102         $this->updated,
103         (int)$this->deleted,
104         $this->pmid,
105         $this->mid);
106     }
107   }
108
109   static function macro_BurnDown() {
110     global $ABSWEB;
111
112     $args = func_get_args();
113
114     if (!count($args) || (count($args) == 1 && $args[0] == '')) {
115       # Special case for allowing burndown to NOP in the milestone summary
116       return '';
117     }
118
119     $params = array(
120       'width' => '75%',
121       'height' => '250px',
122     );
123
124     foreach ($args as $arg) {
125       list($name, $value) = explode('=', $arg, 2);
126       $params[$name] = $value;
127     }
128
129     $m = MTrack_Milestone::loadByName($params['milestone']);
130     if (!$m) {
131       return "BurnDown: milestone $params[milestone] is invalid<br>\n";
132     }
133     if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
134       return "Not authorized to view milestone $name<br>\n";
135     }
136  
137     /* step 1: find all changes on this milestone and its children */
138     $effort = MTrackDB::q("
139       select expended, remaining, changedate
140       from
141         ticket_milestones tm
142       left join
143         effort e on (tm.tid = e.tid)
144       left join
145         changes c on (e.cid = c.cid)
146       where (mid = ? 
147         or (mid in (select mid from milestones where pmid = ?))
148       )
149          and c.changedate is not null
150       order by c.changedate",
151       $m->mid, $m->mid)->fetchAll(PDO::FETCH_NUM);
152
153     /* estimated hours by day */
154     $estimate_by_day = array();
155     /* accumulated work spent by day */
156     $accum_spent_by_day = array();
157     /* accumulated remaining hours by day */
158     $accum_remain_by_day = array();
159
160     $current_estimate = null;
161     $min_day = null;
162     $max_value = 0;
163     $total_exp = 0;
164
165     foreach ($effort as $info) {
166       list($exp, $rem, $date) = $info;
167       list($day, $rest) = explode('T', $date, 2);
168
169       /* previous day estimate carries over to today */
170       if (!isset($estimate_by_day[$day])) {
171         $estimate_by_day[$day] = $current_estimate;
172       }
173
174       /* previous accumulation carries over */
175       if (!isset($accum_spent_by_day[$day])) {
176         $accum_spent_by_day[$day] = $total_exp;
177       }
178
179       /* revise the estimate for today; also applies
180        * to the number we carry over to tomorrow */
181       if ($rem !== null) {
182         $estimate_by_day[$day] += $rem;
183         $current_estimate = $estimate_by_day[$day];
184       }
185
186       if ($exp !== null) {
187         if ($exp != 0 && $min_day === null) {
188           $min_day = strtotime($date);
189         }
190         $accum_spent_by_day[$day] += $exp;
191         $total_exp += $exp;
192       }
193       $accum_remain_by_day[$day] = $current_estimate - $total_exp;
194       $max_value = max($max_value, $current_estimate);
195     }
196
197     $init_estimate = 0;
198     foreach ($estimate_by_day as $v) {
199       if ($v) {
200         $init_estimate = $v;
201         break;
202       }
203     }
204
205     /* limit the view to the past 3 weeks */
206     $earliest = strtotime('-3 week');
207     if ($min_day < $earliest) {
208 //      $min_day = $earliest;
209     }
210     $min_day *= 1000;
211
212     if ($m->duedate) {
213       $maxday = strtotime($m->duedate);
214     } else {
215       $maxday = time();
216     }
217     $maxday = strtotime('1 week', $maxday) * 1000;
218
219     /* step 3: compute the day by day remaining value,
220      * and produce data series for remaining and expended time */
221
222     $js_remain = array();
223     $js_estimate = array();
224     $trend = array();
225     foreach ($accum_remain_by_day as $day => $remaining) {
226
227       /* compute javascript timestamp */
228       list($year, $month, $dayno) = explode('-', $day);
229       $ts = gmmktime(0, 0, 0, $month, $dayno, $year) * 1000;
230
231       $js_remain[] = "[$ts, $remaining]";
232       $est = (int)$estimate_by_day[$day];
233       $js_estimate[] = "[$ts, $est]";
234       $trend[$ts] = $remaining;
235     }
236
237     $js_remain = join(',', $js_remain);
238     $js_estimate = join(',', $js_estimate);
239
240     $flot = "bd_graph_" . sha1(join(':', $args) . time());
241
242     $max_value *= 1.2;
243
244     $height = (int)$params['height'];
245
246     $html = "
247 <div id='$flot' class='flotgraph'
248   style='width: $params[width]; height: $params[height];'></div>
249 <script id='source_$flot' language='javascript' type='text/javascript'>
250 \$(function () {
251   var p = \$('#$flot');
252   // Not sure what's up here, but somehow the height for the element
253   // shows up as 0 in safari, despite the style setting above... so let's
254   // just force the height here.
255   if (p.height() == 0) {
256     p.height($height);
257   }
258   \$.plot(p, [
259     { label: \"estimated\", data: [$js_estimate], yaxis: 1 },
260     { label: \"remaining\", data: [$js_remain] }
261     ], {
262      xaxis: {
263        mode: \"time\",
264        timeformat: '%b %d',
265        min: $min_day,
266        max: $maxday
267      },
268      yaxis: {
269       max: $max_value
270      },
271      legend: {
272       position: 'sw'
273      },
274      grid: {
275       hoverable: true
276      }
277     }
278   );
279 });
280 </script>
281 ";
282
283     $delta = $init_estimate - $total_exp;
284
285     return
286       "<div class='burndown'>Initial estimate: $init_estimate, Work expended: $total_exp<br>\n"
287       . $html . "</div>";
288   }
289
290   static function macro_MilestoneSummary($name) {
291     global $ABSWEB;
292
293     $m = self::loadByName($name);
294     if (!$m) {
295       return "milestone: " . htmlentities($name) . " not found<br>\n";
296     }
297
298     if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
299       return "Not authorized to view milestone $name<br>\n";
300     }
301    
302     $completed = mtrack_date($m->completed);
303     $description = $m->description;
304     if (strpos($description, "[[BurnDown(") === false) {
305       $description = "[[BurnDown(milestone=$name,width=50%,height=150)]]\n" .
306         $description;
307     }
308     $desc = MTrack_Wiki::format_to_html($description);
309     $pname = $name;
310     if ($m->completed !== NULL) {
311       $pname = "<del>$name</del>";
312       $due = "Completed";
313     } elseif ($m->duedate) {
314       $due = "Due " . mtrack_date($m->duedate);
315     } else {
316       $due = null;
317     }
318
319     $watch = MTrackWatch::getWatchUI('milestone', $m->mid);
320
321     $html = <<<HTML
322 <div class="milestone">
323 <h2><a href="{$ABSWEB}milestone.php/$name">$pname</a></h2>
324 $watch
325 <div class="due">$due</div>
326 $desc<br/>
327 HTML;
328
329     $estimated = 0;
330     $remaining = 0;
331     $open = 0;
332     $total = 0;
333
334     foreach (MTrackDB::q('select status, estimated, estimated - spent as remaining from ticket_milestones tm left join tickets t on (tm.tid = t.tid) where mid = ?',
335         $m->mid)->fetchAll(PDO::FETCH_ASSOC) as $row) {
336       $total++;
337       if ($row['status'] != 'closed') {
338         $open++;
339       }
340       $estimated += $row['estimated'];
341       $remaining += $row['remaining'];
342     }
343
344     $closed = $total - $open;
345     if ($total) {
346       $apct = (int)($open / $total * 100);
347     } else {
348       $apct = 0;
349     }
350     $cpct = 100 - $apct;
351     $html .= <<<HTML
352 <table class='progress'>
353 <tr>
354   <td class='closed' style='width:$cpct%;'><a href='#'></a></td>
355 HTML;
356
357     if ($open) {
358       $html .= <<<HTML
359   <td class='open' style='width:$apct%;'><a href='#'> </a></td>
360 HTML;
361     }
362
363     $ms = urlencode($name);
364
365     $html .= <<<HTML
366 </tr>
367 </table>
368 <a href='{$ABSWEB}query.php?milestone=$ms&status!=closed'>$open open</a>,
369 <a href='{$ABSWEB}query.php?milestone=$ms&status=closed'>$closed closed</a>,
370 <a href='{$ABSWEB}query.php?milestone=$ms'>$total total</a> ($cpct % complete)
371 </div>
372 HTML;
373     return $html;
374   }
375 }
376