summary = $summary; $this->settings = $settings; } /** * Install settings. * * @return Settings */ public function settings() { return $this->settings; } /** * Whether a site's reports need the full detail (PDF, secure link or insights). * * @param Profile $site Site. * @return bool */ public static function needs_full( Profile $site ) { $d = $site->delivery; return ! empty( $d['attach_pdf'] ) || ! empty( $d['share_link'] ) || ! empty( $d['insights'] ) || Options::MODE_REVIEW === $d['mode']; } /** * Fetches the numbers and checks them. * * @param Profile $site Site. * @param array $period Period. * @param bool|null $full Full detail; null decides from the site's options. * @param string|null $today Override for "today". * @return array{data:array,readiness:array,insights:array[],full:bool} */ public function prepare( Profile $site, array $period, $full = null, $today = null ) { $full = null === $full ? self::needs_full( $site ) : (bool) $full; $data = $this->summary->build( $site, $period, $full ); $readiness = Readiness::check( $site, $data, $period, $today ); $insights = ! empty( $site->delivery['insights'] ) || Options::MODE_REVIEW === $site->delivery['mode'] ? Insights::generate( $site, $data, $period ) : array(); return array( 'data' => $data, 'readiness' => $readiness, 'insights' => $insights, 'full' => $full, ); } /** * Everything templates and the PDF need for a run. * * @param Profile $site Site. * @param array $run Run with snapshot, checks, insights and notes. * @param bool $test Test send. * @return array */ public function context( Profile $site, array $run, $test = false ) { $period = RunRepository::period( $run ); $snapshot = is_array( $run['snapshot'] ) ? $run['snapshot'] : array(); $data = isset( $snapshot['data'] ) ? (array) $snapshot['data'] : array(); $readiness = is_array( $run['checks'] ) && isset( $run['checks']['level'] ) ? $run['checks'] : array( 'level' => '', 'checks' => array(), ); $meta = is_array( $run['meta'] ) ? $run['meta'] : array(); $frequency = isset( $meta['frequency'] ) ? (string) $meta['frequency'] : ''; $compare = isset( $meta['compare'] ) ? (string) $meta['compare'] : self::compare_of( $period ); $label = self::label( $period, $frequency ); $show = ! empty( $site->delivery['insights'] ) || ! empty( $run['reviewed_at'] ); $notes = trim( (string) $run['notes'] ); $site_note = isset( $site->email['note'] ) ? trim( (string) $site->email['note'] ) : ''; return array( 'site' => $site, 'run' => $run, 'period' => $period, 'data' => $data, 'label' => $label, 'title' => self::title( $label ), 'kind' => self::kind( $period, $frequency ), 'compare' => $compare, 'compare_label' => Insights::versus( $period ), 'readiness' => $readiness, 'problems' => Readiness::problems( $readiness ), 'insights' => $show && is_array( $run['insights'] ) ? $run['insights'] : array(), 'notes' => $notes, 'site_note' => $site_note, 'test' => (bool) $test, 'report_url' => self::report_url( $site, $period, $compare ), 'brand' => array( 'name' => $this->settings->brand_name(), 'color' => $this->settings->brand_color(), 'logo' => $this->settings->logo_url(), 'logo_path' => Settings::attachment_file( (int) $this->settings->get( 'brand_logo_id' ) ), 'client_logo' => Settings::attachment_url( $site->client_logo_id ), 'client_logo_path' => Settings::attachment_file( $site->client_logo_id ), 'footer' => (string) $this->settings->get( 'footer_text' ), 'credit' => $this->settings->show_credit(), ), 'prepared' => wp_date( 'j M Y' ), ); } /** * The PDF for a run. * * @param Profile $site Site. * @param array $run Run (with snapshot). * @return string|\WP_Error PDF bytes. */ public function pdf( Profile $site, array $run ) { if ( ! is_array( $run['snapshot'] ) || empty( $run['snapshot']['full'] ) ) { if ( null === $run['snapshot'] && ! empty( $run['sent_at'] ) ) { return new \WP_Error( 'turtlereport_pdf_gone', __( 'The numbers behind this PDF were cleared after 120 days, so it can\'t be shown again. The recipients still have their copy.', 'turtlereport' ) ); } return new \WP_Error( 'turtlereport_pdf_data', __( 'This report was built without the detail a PDF needs. Prepare it again.', 'turtlereport' ) ); } if ( function_exists( 'wp_raise_memory_limit' ) ) { wp_raise_memory_limit( 'turtlereport_pdf' ); } $ctx = $this->context( $site, $run ); $ctx['notes'] = trim( $ctx['site_note'] . ( '' !== $ctx['site_note'] && '' !== $ctx['notes'] ? "\n\n" : '' ) . $ctx['notes'] ); $ctx['brand'] = array_intersect_key( $ctx['brand'], array_flip( array( 'name', 'color', 'logo_path', 'client_logo_path', 'footer', 'credit' ) ) ); try { return ReportPdf::render( $ctx ); } catch ( \Throwable $e ) { /* translators: %s: error message. */ return new \WP_Error( 'turtlereport_pdf_failed', sprintf( __( 'The PDF couldn\'t be created: %s', 'turtlereport' ), ErrorText::from( $e ) ) ); } } /** * File name for a run's PDF, e.g. "acme-widgets-september-2026.pdf". * * @param Profile $site Site. * @param array $run Run. * @return string */ public static function pdf_name( Profile $site, array $run ) { $meta = is_array( $run['meta'] ) ? $run['meta'] : array(); $name = sanitize_title( $site->display_name() . ' ' . self::label( RunRepository::period( $run ), isset( $meta['frequency'] ) ? (string) $meta['frequency'] : '' ) ); return ( '' !== $name ? $name : 'report' ) . '.pdf'; } /** * Name of the period: "September 2026", "Q3 2026", or a date range. * * @param array $period Period. * @param string $frequency Schedule interval, if known. * @return string */ public static function label( array $period, $frequency = '' ) { $s = $period['s']; $e = $period['e']; if ( '01' === substr( $s, 8 ) && gmdate( 'Y-m-t', strtotime( $s . ' 12:00:00 UTC' ) ) === $e ) { return wp_date( 'F Y', strtotime( $s . ' 12:00:00 UTC' ), new \DateTimeZone( 'UTC' ) ); } $m = (int) substr( $s, 5, 2 ); if ( '01' === substr( $s, 8 ) && 1 === $m % 3 && Periods::add_days( Periods::add_months( $s, 3 ), -1 ) === $e ) { /* translators: 1: quarter number (1-4), 2: year. */ return sprintf( __( 'Q%1$d %2$s', 'turtlereport' ), (int) ceil( $m / 3 ), substr( $s, 0, 4 ) ); } unset( $frequency ); return Format::range( $s, $e ); } /** * Report title: "September 2026 report". * * @param string $label Period name. * @return string */ public static function title( $label ) { /* translators: %s: period name, e.g. "September 2026" or a date range. */ return sprintf( __( '%s report', 'turtlereport' ), $label ); } /** * What the period is: "Full month", "Last 7 days"… * * @param array $period Period. * @param string $frequency Schedule interval. * @return string */ public static function kind( array $period, $frequency = '' ) { switch ( $frequency ) { case Schedule::WEEKLY: return __( 'Last 7 days', 'turtlereport' ); case Schedule::BIWEEKLY: return __( 'Last 14 days', 'turtlereport' ); case Schedule::QUARTERLY: return __( 'Full quarter', 'turtlereport' ); case Schedule::MONTHLY: return __( 'Full month', 'turtlereport' ); } $label = self::label( $period ); return Format::range( $period['s'], $period['e'] ) === $label ? __( 'Custom range', 'turtlereport' ) : __( 'Full period', 'turtlereport' ); } /** * How a period's comparison was chosen: previous or year. * * @param array $period Period. * @return string previous|year */ public static function compare_of( array $period ) { return Periods::shift_year( $period['s'], -1 ) === $period['ps'] && Periods::shift_year( $period['e'], -1 ) === $period['pe'] ? 'year' : 'previous'; } /** * Report screen URL that opens exactly this period and comparison. * * @param Profile $site Site. * @param array $period Period. * @param string $compare previous|year. * @return string */ public static function report_url( Profile $site, array $period, $compare = 'previous' ) { return Links::url( array( 'site' => $site->id, 'start' => $period['s'], 'end' => $period['e'], 'compare' => 'year' === $compare ? 'year' : 'previous', 'cstart' => $period['ps'], 'cend' => $period['pe'], ) ); } }