File Coverage

blib/lib/PHPLive/Report.pm
Criterion Covered Total %
statement 33 115 28.7
branch 6 18 33.3
condition n/a
subroutine 5 10 50.0
pod 2 2 100.0
total 46 145 31.7


line stmt bran cond sub pod time code
1             package PHPLive::Report;
2              
3             our $DATE = '2014-08-16'; # DATE
4             our $VERSION = '0.04'; # VERSION
5              
6 1     1   976 use 5.010;
  1         3  
  1         43  
7 1     1   6 use strict;
  1         2  
  1         137  
8 1     1   6 use warnings;
  1         2  
  1         30  
9 1     1   2238 use Log::Any '$log';
  1         2893  
  1         5  
10              
11             require Exporter;
12             our @ISA = qw(Exporter);
13             our @EXPORT_OK = qw(parse_phplive_transcript %reports %legends);
14              
15             our %SPEC;
16              
17             our %reports = (
18             chat_report => 'Chat reports',
19             chat_report_by_dept => 'Chat report by department',
20             chat_report_by_op => 'Chat report by operator',
21             );
22              
23             our %legends = (
24             pct_rated => 'Percentage of chats that are rated by clients',
25             pct_has_transfers => 'Percentage of chats that involve a transfer of operators',
26             avg_chat_duration => 'Average chat duration, in minutes',
27             avg_rating => 'Average chat rating from clients (counted over chats that are rated only)',
28             num_chats => 'Number of chats',
29             avg_msg_lines => 'Average number of message lines in a single chat. Note that non-message lines are excluded',
30             avg_msg_words => 'Average number of words in a single chat. Note that the username prefix in message lines and non-message lines are excluded',
31             avg_msg_chars => 'Average number of characters in a single chat. Like in "avg_msg_words", the username prefix in message lines and non-message lines are excluded',
32              
33             avg_simul_chats => 'Average number of simultaneous chats held by the operator at a time',
34             );
35              
36              
37             $SPEC{parse_phplive_transcript} = {
38             v => 1.1,
39             description => <<'_',
40              
41             The `plain` column in `p_transcripts` table stores the actual chat transcript.
42             Entities characters like `<` and `>` are HTML-entities-escaped (becoming `&lt;`
43             and `&gt;`). Multiple lines are squished together into a single line. No
44             timestamp is recorded for each message.
45              
46             _
47             args => {
48             transcript => {schema=>'str*', req=>1, pos=>0},
49             },
50             args_as => 'array',
51             result_naked => 1,
52             };
53             sub parse_phplive_transcript {
54 1     1 1 9 my ($transcript) = @_;
55              
56 1         14 my @lines = split /^/m, $transcript;
57 1         3 my $num_transfers = 0;
58 1         3 my $num_msg_lines = 0;
59 1         2 my $num_msg_words = 0;
60 1         2 my $num_msg_chars = 0;
61              
62 1         2 my %operators;
63 1         4 for (@lines) {
64 30 100       70 if (/^(.+) has joined the chat\.$/) {
65 2         5 $operators{$1}++;
66 2         5 next;
67             }
68 28 100       99 if (/(.+?): (.+)/) {
69 25         24 $num_msg_lines++;
70 25         55 chomp(my $msg = $2);
71 25         279 $num_msg_words++ while $msg =~ /(\w+)/g;
72 25         33 $num_msg_chars += length($msg);
73 25         48 next;
74             }
75 3 100       11 if (/^Transferring chat to /) {
76 1         2 $num_transfers++;
77 1         3 next;
78             }
79             }
80              
81             return {
82 1         21 num_transfers => $num_transfers,
83             num_operators => scalar(keys %operators),
84             num_msg_lines => $num_msg_lines,
85             num_msg_words => $num_msg_words,
86             num_msg_chars => $num_msg_chars,
87             };
88             }
89              
90             sub _recap_transcripts {
91 0     0     my ($transcripts, $filter) = @_;
92              
93 0           my $n = 0;
94 0           my $has_transfers = 0;
95 0           my $total_msg_lines = 0;
96 0           my $total_msg_words = 0;
97 0           my $total_msg_chars = 0;
98 0           for my $k (keys %$transcripts) {
99 0           my $t = $transcripts->{$k};
100 0 0         next unless $filter->($t);
101 0           $n++;
102 0 0         $has_transfers++ if $t->{num_transfers};
103 0           $total_msg_lines += $t->{num_msg_lines};
104 0           $total_msg_words += $t->{num_msg_words};
105 0           $total_msg_chars += $t->{num_msg_chars};
106             }
107             return {
108             #num_transcripts => $n,
109 0 0         pct_has_transfers => $n == 0 ? 0 : sprintf("%.2f", $has_transfers/$n*100.0),
    0          
    0          
    0          
110             avg_msg_lines => $n == 0 ? 0 : sprintf("%.f", $total_msg_lines/$n),
111             avg_msg_words => $n == 0 ? 0 : sprintf("%.f", $total_msg_words/$n),
112             avg_msg_chars => $n == 0 ? 0 : sprintf("%.f", $total_msg_chars/$n),
113             };
114             }
115              
116             $SPEC{gen_phplive_reports} = {
117             v => 1.1,
118             args => {
119             dbh => {schema=>'obj*', req=>1},
120             year => {schema=>'int*', req=>1},
121             month => {schema=>['int*', between=>[1,12]], req=>1},
122             },
123             result_naked=>1,
124             };
125             sub gen_phplive_reports {
126 0     0 1   require DateTime;
127              
128 0           my %args = @_;
129              
130 0           my $res;
131              
132 0           my $dbh = $args{dbh};
133 0           my $year = $args{year}+0;
134 0           my $month = $args{month}+0;
135              
136 0           my $dt = DateTime->new(year=>$year, month=>$month, day=>1);
137 0           my $ts_start_of_month = $dt->epoch;
138 0           $dt->add(months => 1)->subtract(seconds => 1);
139 0           my $ts_end_of_month = $dt->epoch;
140              
141 0           my $sql;
142             my $sth;
143              
144 0           $log->debug("Parsing all transcripts ...");
145 0           $sql = <<_;
146             SELECT
147             ces,
148             opID,
149             deptID,
150             plain transcript
151             FROM p_transcripts
152             WHERE created BETWEEN $ts_start_of_month AND $ts_end_of_month
153             _
154 0           $sth = $dbh->prepare($sql);
155 0           $sth->execute;
156 0           my %transcripts; # key = ces (table PK)
157 0           while (my $row = $sth->fetchrow_hashref) {
158 0           my $res = parse_phplive_transcript($row->{transcript});
159             # insert this so we can recapitulate on a per-department/per-operator
160             # basis
161 0           $res->{opID} = $row->{opID};
162 0           $res->{deptID} = $row->{deptID};
163 0           $transcripts{$row->{ces}} = $res;
164             }
165              
166 0           $log->debug("Preparing chat reports ...");
167 0           my $sql_cr = <<_;
168             COUNT(*) num_chats,
169             ROUND(AVG(t.ended-t.created)/60, 1) avg_chat_duration,
170             IF(COUNT(*)=0,0,ROUND(SUM(IF(t.rating>0,1,0))/COUNT(*)*100.0,2)) pct_rated,
171             IF(SUM(IF(t.rating>0,1,0))=0,0,ROUND(SUM(t.rating)/SUM(IF(t.rating>0,1,0)),2)) avg_rating
172             _
173 0           $sql = <<_;
174             SELECT
175             $sql_cr
176             FROM p_transcripts t
177             WHERE created BETWEEN $ts_start_of_month AND $ts_end_of_month
178             _
179 0           $sth = $dbh->prepare($sql);
180 0           $sth->execute;
181             {
182 0           my @rows;
  0            
183 0           while (my $row = $sth->fetchrow_hashref) {
184 0           push @rows, $row;
185 0     0     my $tres = _recap_transcripts(\%transcripts, sub{1});
  0            
186 0           $row->{$_} = $tres->{$_} for keys %$tres;
187             }
188 0           $res->{chat_report} = \@rows;
189             }
190              
191 0           $log->debug("Preparing per-department chat reports ...");
192 0           $sql = <<_;
193             SELECT
194             t.deptID deptID,
195             (SELECT name FROM p_departments WHERE deptID=t.deptID) deptName,
196             $sql_cr
197             FROM p_transcripts t
198             WHERE created BETWEEN $ts_start_of_month AND $ts_end_of_month
199             GROUP BY t.deptID
200             _
201 0           $sth = $dbh->prepare($sql);
202 0           $sth->execute;
203             {
204 0           my @rows;
  0            
205 0           while (my $row = $sth->fetchrow_hashref) {
206 0           push @rows, $row;
207 0     0     my $tres = _recap_transcripts(
208 0           \%transcripts, sub{shift->{deptID} == $row->{deptID}});
209 0           $row->{$_} = $tres->{$_} for keys %$tres;
210             # so they are the first/leftmost columns
211 0           $row->{'00deptID'} = $row->{deptID};
212 0           delete $row->{deptID};
213 0           $row->{'00deptName'} = $row->{deptName};
214 0           delete $row->{deptName};
215             }
216 0           $res->{chat_report_by_dept} = \@rows;
217             }
218              
219 0           $log->debug("Preparing per-operator chat reports ...");
220 0           $sql = <<_;
221             SELECT
222             t.opID opID,
223             (SELECT name FROM p_operators WHERE opID=t.opID) opName,
224             $sql_cr
225             FROM p_transcripts t
226             WHERE created BETWEEN $ts_start_of_month AND $ts_end_of_month
227             GROUP BY t.opID
228             _
229 0           $sth = $dbh->prepare($sql);
230 0           $sth->execute;
231             {
232 0           my @rows;
  0            
233 0           while (my $row = $sth->fetchrow_hashref) {
234 0           push @rows, $row;
235 0     0     my $tres = _recap_transcripts(
236 0           \%transcripts, sub{shift->{opID} == $row->{opID}});
237 0           $row->{$_} = $tres->{$_} for keys %$tres;
238             # so they are the first/leftmost columns
239 0           $row->{'00opID'} = $row->{opID};
240 0           delete $row->{opID};
241 0           $row->{'00opName'} = $row->{opName};
242 0           delete $row->{opName};
243             }
244 0           $res->{chat_report_by_op} = \@rows;
245             }
246              
247 0           $res;
248             }
249              
250             1;
251             # ABSTRACT: Generate reports for PHP Live!
252              
253             __END__
254              
255             =pod
256              
257             =encoding UTF-8
258              
259             =head1 NAME
260              
261             PHPLive::Report - Generate reports for PHP Live!
262              
263             =head1 VERSION
264              
265             This document describes version 0.04 of PHPLive::Report (from Perl distribution PHPLive-Report), released on 2014-08-16.
266              
267             =head1 SYNOPSIS
268              
269             Use the included L<gen-phplive-reports> to generate HTML report files.
270              
271             =head1 DESCRIPTION
272              
273             PHP Live! is a web-based live chat/live support application,
274             L<http://www.phplivesupport.com/>. As of this writing, version 4.4.7, the
275             reports it generates are quite limited. This module produces additional reports
276             for your PHP Live! installation.
277              
278             =head1 FUNCTIONS
279              
280              
281             =head2 gen_phplive_reports(%args) -> any
282              
283             Arguments ('*' denotes required arguments):
284              
285             =over 4
286              
287             =item * B<dbh>* => I<obj>
288              
289             =item * B<month>* => I<int>
290              
291             =item * B<year>* => I<int>
292              
293             =back
294              
295             Return value:
296              
297             (any)
298              
299              
300             =head2 parse_phplive_transcript($transcript) -> any
301              
302             The C<plain> column in C<p_transcripts> table stores the actual chat transcript.
303             Entities characters like C<< E<lt> >> and C<< E<gt> >> are HTML-entities-escaped (becoming C<&lt;>
304             and C<&gt;>). Multiple lines are squished together into a single line. No
305             timestamp is recorded for each message.
306              
307             Arguments ('*' denotes required arguments):
308              
309             =over 4
310              
311             =item * B<transcript>* => I<str>
312              
313             =back
314              
315             Return value:
316              
317             (any)
318              
319             =head1 TODO
320              
321             =over
322              
323             =item * Add operator reports
324              
325             avg_simul_chats
326              
327             =item * Add department reports
328              
329             =item * Add report: monthly number of chats against hour of day
330              
331             With bar chart.
332              
333             =item * Add report: number of chats against day of week
334              
335             With bar chart.
336              
337             =back
338              
339             =head1 HOMEPAGE
340              
341             Please visit the project's homepage at L<https://metacpan.org/release/PHPLive-Report>.
342              
343             =head1 SOURCE
344              
345             Source repository is at L<https://github.com/sharyanto/perl-PHPLive-Report>.
346              
347             =head1 BUGS
348              
349             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=PHPLive-Report>
350              
351             When submitting a bug or request, please include a test-file or a
352             patch to an existing test-file that illustrates the bug or desired
353             feature.
354              
355             =head1 AUTHOR
356              
357             Steven Haryanto <stevenharyanto@gmail.com>
358              
359             =head1 COPYRIGHT AND LICENSE
360              
361             This software is copyright (c) 2014 by Steven Haryanto.
362              
363             This is free software; you can redistribute it and/or modify it under
364             the same terms as the Perl 5 programming language system itself.
365              
366             =cut