#!/usr/bin/perl -w

# OTRS-Projekt: TODO: Umstellung auf Ticket-API

use utf8;
use strict;
use warnings;
use utf8;

BEGIN {
    unshift @INC, ( $ENV{'POPHOME'} || '@POPHOME@' ) . '/lib'
      unless $ENV{'KUNDE_NO_PERLPATH'};
}

use Dbase::Getopt qw(:DEFAULT :kunden getopt_date getopt_descr getopt_person);
use Dbase::Globals qw(find_descr flag_names get_descr time4ticket);

# ":readonly" scheidet erstmal aus,
# da time4ticket() evtl. in stunden.zeit schreiben möchte:
use Dbase::Help qw(DoFn DoSelect DoTrans in_list in_range like_list qquote);
use Loader qw(verbundene_tickets);
use Memoize qw(memoize);
use noris::CSV;
use Umlaut qw(textmodus);

use constant OHNE_MIT => map "Arbeitszeit $_ Faktoren (s)", qw(ohne mit);
use constant ALLE_TICKETDATEN => (
    \'geplante Arbeitszeit',
    qw(Bearbeiter Priorität Kunde Status Queue),
    \'Area',
    qw(Subject Info),
    \'Conf.Items',
    \'Leitungen',
    \'Dauer bis Beginn der Störungsanalyse (s)',
    \'Wiederherstellungszeit (s)',
    \'Störungsart',
    \'Störungsflags',
);
use constant TICKETDATEN => ( map ref() ? $$_ : $_, ALLE_TICKETDATEN );
use constant OPTIONALE_TICKETDATEN => ( map $$_, grep ref(), ALLE_TICKETDATEN );

memoize('flag_names');

die <<_ unless @ARGV;
Nein, ich werde jetzt _nicht_ die Arbeitszeit _aller_ Tickets ermitteln.
_

my (
    $TicketsSeit,    $TicketsVor, $ZeitSeit,   $ZeitVor,
    %Antwort,        @Status,     @OhneStatus, @Bearbeiter,
    @OhneBearbeiter, $Sollzeit,   $Verbundene,
);
my $CheckKunde = my $Summenzeile = 1;
GetOptions(
    'ticket=i'                      => \my @TicketIDs,
    'ohne-ticket=i'                 => \my @OhneTicketIDs,
    'tickets-seit=s'                => sub { $TicketsSeit = &getopt_date },
    'tickets-vor=s'                 => sub { $TicketsVor = &getopt_date },
    'zeit-seit=s'                   => sub { $ZeitSeit = &getopt_date },
    'zeit-vor=s'                    => sub { $ZeitVor = &getopt_date },
    'zeit-fuer-ausgewaehlte-kunden' => sub { $CheckKunde = 1 },
    'zeit-fuer-alle-kunden|nocheck-kunde' => sub { $CheckKunde = '' },
    'conf-item=s'                         => \my @ConfItems,
    'ohne-conf-item=s'                    => \my @OhneConfItems,
    'leitung=s'                           => \my @Leitungen,
    'ohne-leitung=s'                      => \my @OhneLeitungen,
    'antwort=s%'                          => sub {
        my ( undef, $frage, $antwort ) = @_;
        $Antwort{
            $frage =~ /\D/
            ? find_descr( rt_fragen => $frage, 1 )
            : $frage
          }
          = $antwort;
    },
    'bearbeiter=s'      => sub { push @Bearbeiter,     &getopt_person },
    'ohne-bearbeiter=s' => sub { push @OhneBearbeiter, &getopt_person },
    'abteilung-like=s' => \my @AbteilungLike,
    'max-priority=i' => \my $MaxPriority,
    'min-priority=i' => \my $MinPriority,
    'status=s'       => sub { push @Status, getopt_descr( tickets => @_ ) },
    'ohne-status=s'  => sub { push @OhneStatus, getopt_descr( tickets => @_ ) },
    'queue=s'        => \my @Queues,
    'ohne-queue=s'   => \my @OhneQueues,
    'area=s'         => \my @Areas,
    'ohne-area=s'    => \my @OhneAreas,
    'info-like=s'    => \my @InfoLike,
    'subject-like=s' => \my @SubjectLike,
    'nur-sichtbare'  => \my $NurSichtbare,
    'min-sollzeit=f' => \$Sollzeit,
    'ohne-sollzeit'      => sub { $Sollzeit   = '' },
    'addiere-verbundene' => sub { $Verbundene = '' },
    'zeige-verbundene'   => sub { $Verbundene = 1 },
    'aufrunden=i'       => \my $Aufrunden,
    'summenzeile'       => sub { $Summenzeile = 1 },
    'keine-summenzeile' => sub { $Summenzeile = '' },
    'csv-option=s'      => \my %CSV_Options,
    'debug-sql'         => \my $DebugSQL,
);

$_ eq '' and $_ = undef for @Areas, @OhneAreas;
if ($Aufrunden) {
    require POSIX and POSIX->import('ceil');
    $Aufrunden *= 60;
}

my @add_from;
my @where = (
    in_list( 'queue_areas.name',  '', @Areas ),
    in_list( 'queue_areas.name',  NOT => @OhneAreas ),
    in_list( 'ticket.bearbeiter', '', @Bearbeiter ),
    in_list( 'ticket.bearbeiter', NOT => @OhneBearbeiter ),
    in_list( 'ticket.kunde',      '', @Kunden ),
    in_list( 'ticket.kunde',      NOT => @OhneKunden ),
    in_list( 'queue.name',        '', @Queues ),
    in_list( 'queue.name',        NOT => @OhneQueues ),
    in_list( 'confitem.name',     '', @ConfItems ),
    in_list( 'leitung.name',      '', @Leitungen ),
    in_list( 'ticket.status',     '', @Status ),
    in_list( 'ticket.status',     NOT => @OhneStatus ),
    like_list( 'person.abt',      '', @AbteilungLike ),
    like_list( 'ticket.infotext', '', @InfoLike ),
    like_list( 'ticket.subject',  '', @SubjectLike ),
    in_range( 'ticket.wichtig', $MinPriority, $MaxPriority ),
);
push @where, "ticket.beginn >= $TicketsSeit" if defined $TicketsSeit;
push @where, "ticket.beginn < $TicketsVor"   if defined $TicketsVor;
push @where,
  $Sollzeit eq '' ? 'ticket.zeit IS NULL' : "ticket.zeit >= 3600 * $Sollzeit"
  if defined $Sollzeit;
push @where,
  '0 = ( SELECT COUNT(*) '
  . 'FROM rt_incidents_confitem not_ric, confitem not_ci '
  . 'WHERE not_ric.incident = ticket.id AND not_ci.id = not_ric.confitem AND '
  . in_list( 'not_ci.name', '', @OhneConfItems ) . ' )'
  if @OhneConfItems;
push @where,
  '0 = ( SELECT COUNT(*) '
  . 'FROM rt_incidents_leitung not_ril, leitung not_lt '
  . 'WHERE not_ril.incident = ticket.id AND not_lt.id = not_ril.leitung AND '
  . in_list( 'not_lt.name', '', @OhneLeitungen ) . ' )'
  if @OhneLeitungen;

if ( @TicketIDs || @OhneTicketIDs || $NurSichtbare ) {
    push @add_from, 'ticket ticket_id';
    push @where,    'ticket_id.ticket = ticket.id',
      in_list( 'ticket_id.id', '', @TicketIDs ),
      in_list( 'ticket_id.id', NOT => @OhneTicketIDs );
    if ($NurSichtbare) {
        push @add_from, 'ticketid';
        push @where, 'ticketid.ticket = ticket_id.id', 'ticketid.extern = "y"';
    }
}

# Nicht unnötig Tickets ohne einschlägige Zeitbuchung auswählen:
if ( !defined $Verbundene and defined $ZeitSeit || defined $ZeitVor ) {
    push @add_from, 'stunden', 'ticket t';
    push @where, 'stunden.ticket = t.id', 't.ticket = ticket.id';
    push @where, "stunden.beginn >= $ZeitSeit" if defined $ZeitSeit;
    push @where, "stunden.beginn + stunden.dauer < $ZeitVor"
      if defined $ZeitVor;
    push @where, in_list( 'stunden.kunde', '', @Kunden ),
      in_list( 'stunden.kunde', NOT => @OhneKunden )
      if $CheckKunde;
}

while ( my ( $frage_id, $erwartete_antwort ) = each %Antwort ) {
    push @add_from, "rt_antwort rf$frage_id";
    push @where,    "rf$frage_id.ticket = ticket.id",
      "rf$frage_id.antwort = ${\ qquote($erwartete_antwort) }";
}

textmodus( \*STDOUT );
my $csv = noris::CSV->new( \%CSV_Options );
print $csv->as_decoded_string(
    defined $Verbundene ? 'Ticketgruppe' : (),
    Ticket => OHNE_MIT,
    TICKETDATEN
);

sub select_tickets {
    my ( $sub, $details, $where, @add_from ) = @_;
    my $sql =
      <<'1' . ( $details ? <<'2' : '' ) . <<3 . join( '', map <<4, @$where ) . <<'5';
        SELECT    ticket.id
1
                  ,
                  ticket.zeit,
                  person.user,
                  ticket.wichtig,
                  kunde.name,
                  ticket.status,
                  queue.name,
                  queue_areas.name,
                  ticket.subject,
                  ticket.infotext,
                  GROUP_CONCAT( DISTINCT  confitem.name
                                ORDER BY  confitem.name
                                SEPARATOR ' '
                              ),
                  GROUP_CONCAT( DISTINCT  leitung.name
                                ORDER BY  leitung.name
                                SEPARATOR ' '
                              ),
	          rt_incidents.t_reaktion    - rt_incidents.t_meldung,
	          rt_incidents.t_entstoerung - rt_incidents.t_meldung,
	          rt_incidents.art,
		  rt_incidents.flags
2
        FROM      ( kunde, queue, ticket${\ join '', map ", $_", @add_from } )
        LEFT JOIN person
               ON person.id                      = ticket.bearbeiter
        LEFT JOIN queue_areas
               ON queue_areas.id                 = ticket.queue_area
        LEFT JOIN rt_incidents_confitem
               ON rt_incidents_confitem.incident = ticket.id
        LEFT JOIN confitem
               ON confitem.id                    = rt_incidents_confitem.confitem
        LEFT JOIN rt_incidents_leitung
               ON rt_incidents_leitung.incident  = ticket.id
        LEFT JOIN leitung
               ON leitung.id                     = rt_incidents_leitung.leitung
	LEFT JOIN rt_incidents
	       ON rt_incidents.ticket            = ticket.id
        WHERE     kunde.id                       = ticket.kunde
              AND queue.id                       = ticket.queue
              AND ticket.id                      = ticket.ticket
3
              AND $_
4
        GROUP BY  ticket.id
        ORDER BY  kunde.name, ticket.id
5
    print '-' x 79, "\n$sql", '-' x 79, "\n" if $DebugSQL;
    DoSelect {
        my %ticketdaten;
        ( my $ticket_id, @ticketdaten{ (TICKETDATEN) } ) = @_;

        defined() and $_ = flag_names( $_, 'rt_incidents_flags' )
          for $ticketdaten{'Störungsflags'};

        if ($details) {
            defined and s#$/# #g for values %ticketdaten;
            defined or $_ = '' for @ticketdaten{ (OPTIONALE_TICKETDATEN) };
            $ticketdaten{Status} = get_descr( tickets => $ticketdaten{Status} );
        }

        @ticketdaten{ (OHNE_MIT) } =
          time4ticket( $ticket_id, $ZeitSeit, $ZeitVor,
            $CheckKunde ? ( \@Kunden, \@OhneKunden ) : () );

        $sub->( $ticket_id, %ticketdaten );
    }
    $sql;
}

sub zeitwerte(%) {
    my %ticketdaten = @_;
    map sprintf( '%.f', $_ ), @ticketdaten{ (OHNE_MIT) };
}

my %time4tickets = map +( $_ => 0 ), OHNE_MIT;

sub verarbeite(\%$;$) {
    my ( $ticketdaten, $ticket_id, $ticketgruppe_id ) = @_;

    if ($Aufrunden) {
        $_ = $Aufrunden * ceil( $_ / $Aufrunden )
          for @{$ticketdaten}{ (OHNE_MIT) };
    }

    $time4tickets{$_} += $ticketdaten->{$_} for OHNE_MIT;

    print $csv->as_decoded_string(
        defined $Verbundene ? $ticketgruppe_id : (),
        $ticket_id,
        zeitwerte(%$ticketdaten),
        @{$ticketdaten}{ (TICKETDATEN) }
      )
      if !defined $ZeitSeit && !defined $ZeitVor
      || grep $ticketdaten->{$_} != 0, OHNE_MIT;
}

my ( %verbundenes_ticket, %ticketgruppe );
DoTrans {

    select_tickets(
        sub {
            my ( $ticket_id, %ticketdaten ) = @_;

            if ( defined $Verbundene ) {
                my $ticketgruppe = $verbundenes_ticket{$ticket_id};
                unless ( defined $ticketgruppe ) {
                    ($ticketgruppe) = my @verbundene_tickets =
                      verbundene_tickets( $ticket_id,
                        [ keys %verbundenes_ticket ] );
                    @verbundenes_ticket{@verbundene_tickets} =
                      ($ticketgruppe) x @verbundene_tickets;
                }
                $ticketgruppe{$ticketgruppe}{$ticket_id} = \%ticketdaten;
            }
            else { verarbeite( %ticketdaten, $ticket_id ) }
        },
        1,
        \@where,
        @add_from
    );

    if ( defined $Verbundene ) {

        delete @verbundenes_ticket{ keys %$_ } for values %ticketgruppe;

        select_tickets(
            $Verbundene
            ? (
                sub {
                    my ( $ticket_id, %ticketdaten ) = @_;
                    $ticketgruppe{ $verbundenes_ticket{$ticket_id} }
                      {$ticket_id} = \%ticketdaten;
                },
                1
              )
            : (
                sub {
                    my ( $ticket_id, %ticketdaten ) = @_;
                    return unless grep $_ != 0, @ticketdaten{ (OHNE_MIT) };
                    my $ticketgruppe =
                      $ticketgruppe{ $verbundenes_ticket{$ticket_id} };
                    my $tickets = keys %$ticketgruppe;
                    for my $spalte (OHNE_MIT) {
                        my $zeit = $ticketdaten{$spalte} / $tickets;
                        $_->{$spalte} += $zeit for values %$ticketgruppe;
                    }
                },
                ''
            ),
            [ in_list( 'ticket.id', '', keys %verbundenes_ticket ) ]
        ) if keys %verbundenes_ticket;

        for my $ticketgruppe_id ( sort { $a <=> $b } keys %ticketgruppe ) {
            verarbeite( %{ $ticketgruppe{$ticketgruppe_id}{$_} },
                $_, $ticketgruppe_id )
              for sort { $a <=> $b } keys %{ $ticketgruppe{$ticketgruppe_id} };
        }
    }
};

print $csv->as_decoded_string( ('') x ( $Verbundene ? 2 : 1 ),
    zeitwerte(%time4tickets) )
  if $Summenzeile;

__END__

=head1 NAME

arbeitszeit4tickets - werte auf RT-Tickets gebuchte Arbeitszeit aus

=head1 BESCHREIBUNG

Gibt eine CSV-Liste aller Tickets aus, auf die (ggf. im gewünschten Zeitraum)
Arbeitszeit gebucht wurde.

=head1 OPTIONEN

=over 4

=item -ticket ID

Selektion nach Ticket-ID(s); bitte die Option mehrfach verwenden, um mehrere
Tickets auszuwählen!
Funktioniert auch für Tickets, die mit anderen Tickets zusammengefasst wurden.

=item -ohne-ticket ID

Selektion nach Ticket-ID(s); bitte die Option mehrfach verwenden, um mehrere
Tickets auszuschließen!
Funktioniert auch für Tickets, die mit anderen Tickets zusammengefasst wurden.

=item -tickets-seit Datum

nur Tickets beachten, die seit dem angegebenen Datum entstanden sind.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -tickets-vor Datum

nur Tickets beachten, die vor dem angegebenen Datum entstanden sind.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -zeit-seit Datum

nur Zeiterfassungseinträge ab dem angegebenen Datum beachten.
Bei Verwendung dieser Option werden nur Tickets selektiert, auf die im
fraglichen Zeitraum mindestens ein Zeiterfassungseintrag gebucht wurde.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -zeit-vor Datum

nur Zeiterfassungseinträge vor dem angegebenen Datum beachten.
Bei Verwendung dieser Option werden nur Tickets selektiert, auf die im
fraglichen Zeitraum mindestens ein Zeiterfassungseintrag gebucht wurde.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -zeit-fuer-ausgewaehlte-kunden

nur Zeiterfassungseinträge beachten, die auf einen der mit L</-kunde> und/oder
L</-ohne-kunde> ausgewählten Kunden gebucht wurden (Default).

=item -zeit-fuer-alle-kunden

Zeiterfassungseinträge unabhängig vom Kunden, auf den sie gebucht wurden,
beachten

=item -bearbeiter Person

Selektion nach Ticket-Bearbeiter(n); bitte die Option mehrfach verwenden, um
Tickets unterschiedlicher Bearbeiter auszuwählen!
Durch Angabe einer leeren Zeichenkette als Argument können Tickets ausgewählt
werden, die keinen Bearbeiter haben.

=item -ohne-bearbeiter Person

Ausschluss von Ticket-Bearbeiter(n); bitte die Option mehrfach verwenden, um
Tickets unterschiedlicher Bearbeiter auszuschließen!
Durch Angabe einer leeren Zeichenkette als Argument können Tickets ohne
Bearbeiter ausgeschlossen werden.

=item -abteilung-like SQL-Wildcard

Selektion nach Abteilung/Team des Ticket-Bearbeiters, also z. B.

 	-abteilung-like 'Technik%'
oder
	-abteilung-like 'Technik, Team Entwicklung'

Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, bei denen
die Abteilung des Bearbeiters zu mindestens einer der angegebenen SQL-Wildcards
passt.

=item -ap-technik Person

=item -ap-vertrieb Person

=item -kunde Kunde

=item -kunde-und-unterkunden Kunde

=item -ohne-ap-technik Person

=item -ohne-ap-vertrieb Person

=item -ohne-kunde Kunde

=item -ohne-kunde-und-unterkunden Kunde

Per Default werden Tickets aller Kunden ausgewertet.
Dies lässt sich vermittels dieser Optionen modifizieren.
Details dazu siehe L<Dbase::Getopt/:kunden>.

=item -queue RT-Queue

Selektion nach RT-Queue(s); bitte die Option mehrfach verwenden,
um mehrere Queues zu selektieren!

=item -ohne-queue RT-Queue

Ausschluss von RT-Queue(s); bitte die Option mehrfach verwenden,
um mehrere Queues auszuschließen!

=item -area Area

Selektion nach RT-Area(s); bitte die Option mehrfach verwenden,
um mehrere Areas auszuwählen!
Um Tickets ohne Area auszuwählen, bitte eine leere Zeichenkette als
Argument übergeben!

=item -ohne-area Area

Ausschluss von RT-Area(s); bitte die Option mehrfach verwenden,
um mehrere Areas auszuschließen!
Um Tickets ohne Area auszuschließen, bitte eine leere Zeichenkette als
Argument übergeben!

=item -info-like SQL-Wildcard

Selektion nach Ticket-Infotext.
Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, deren
Infotext zu mindestens einer der angegebenen SQL-Wildcards passt.

=item -subject-like SQL-Wildcard

Selektion nach Ticket-Subject.
Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, deren
Subject zu mindestens einer der angegebenen SQL-Wildcards passt.

=item -status open

=item -status stalled

=item -status resolved

=item -status dead

nur Tickets beachten, die aktuell einen entsprechenden Status haben.
Kann mehrfach verwendet werden, um Tickets mit unterschiedlichen Status zu
beachten.

=item -ohne-status open

=item -ohne-status stalled

=item -ohne-status resolved

=item -ohne-status dead

Tickets, die aktuell einen entsprechenden Status haben, ausschließen.
Kann mehrfach verwendet werden, um Tickets mit unterschiedlichen Status
auszuschließen.

=item --min-priority <Priorität>

Selektion auf Tickets ab einer bestimmten Mindest-Priorität einschränken.

=item --max-priority <Priorität>

Selektion auf Tickets bis zu einer bestimmten Maximal-Priorität einschränken.

=item -min-sollzeit Stunden

nur Tickets, bei denen mindestens die angegeben Sollzeit eingetragen ist

=item -nur-sichtbare

Nur Tickets auswählen, die für den Kunden (auf service.noris.net) freigegeben
sind.

=item -ohne-sollzeit

nur Tickets ohne Sollzeiteintragung

=item -conf-item Conf.Item

Selektion nach den Tickets zugeordneten Configuration Items, also nur Tickets
auswählen, denen mindestens eines der angegebenen Configuration Items zugeordnet
ist

=item -ohne-conf-item Conf.Item

Ausschluss von Tickets, denen mindestens eines der angegebenen Configuration
Items zugeordnet ist

=item -leitung Leitungsname

Selektion nach den Tickets zugeordneten Leitungen, also nur Tickets auswählen,
denen mindestens eine der angegebenen Leitnugen zugeordnet ist

=item -ohne-leitung Leitungsname

Ausschluss von Tickets, denen mindestens eine der angegebenen Leitungen
zugeordnet ist

=item -antwort Frage=Antwort

nur Tickets betrachten, bei denen die (über die numerische ID oder die
Kurzbezeichnung des C<rt_fragen>-Deskriptors) angegebene Frage mit der Antwort
beantwortet wurde, also z. B.

	-antwort problem=J

Bei mehrfacher Verwendung der Option müssen pro Ticket alle Antworten
übereinstimmen.

=item -zeige-verbundene

mit den ausgewählten Tickets verbundene Tickets ebenfalls anzeigen.
Es wird außerdem eine zusätzliche Spalte C<Ticketgruppe> angezeigt,
die für alle miteinander verbundenen Tickets dieselbe Kennnummer enthält.

Schließt L</-addiere-verbundene> aus.

=item -addiere-verbundene

auf mit ausgewählten Tickets verbundene Tickets gebuchte Arbeitszeit zu den
ausgewählten Tickets addieren.
Arbeitszeit von Tickets, die mit mehreren ausgewählten Tickets verbunden sind,
wird dabei gleichmäßig auf diese Tickets verteilt; die Summe wird anschließend
auf ganze Sekunden gerundet.

Schließt L</-zeige-verbundene> aus.

=item -aufrunden Minuten

Zeiteintragungen pro Ticket(gruppe) in den angegebenen Minutenschritten
aufrunden

=item -keine-summenzeile

um die abschließende Summenzeile der Arbeitszeiten zu unterbinden

=item -csv-option Name=Wert

Optionen für L<Text::CSV_XS>, s. Dokumentation dieses Moduls

=item -debug-sql

um das zur Selektion der Tickets verwendete SQL-Statement zu Debugging-Zwecken
mit auszugeben

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=back
