user_id ); if ( ! $user ) { return; } DevisQR_Texts::begin_customer_output(); $mail = self::build_customer_received( $quote, DevisQR_Database::get_quote_items( $quote->id ), $user->display_name ); DevisQR_Texts::end_customer_output(); wp_mail( $user->user_email, $mail['subject'], $mail['html'], self::headers(), self::pdf_attachment( $quote ) ); } /** * Admin: new request. * * @param object $quote Quote. */ public static function send_new_quote_admin( $quote ) { $s = DevisQR_Settings::resolved(); DevisQR_Texts::begin_customer_output(); $mail = self::build_admin_new( $quote, DevisQR_Database::get_quote_items( $quote->id ) ); DevisQR_Texts::end_customer_output(); $recipients = array_map( 'trim', explode( ',', $s['notification_email'] ) ); $headers = array( 'Content-Type: text/html; charset=UTF-8' ); $user = get_userdata( $quote->user_id ); if ( $user && is_email( $user->user_email ) ) { $headers[] = 'Reply-To: ' . $user->user_email; // Answer the customer directly from the mailbox. } wp_mail( $recipients, $mail['subject'], $mail['html'], $headers, self::pdf_attachment( $quote ) ); } /** * Customer: status changed. * * @param object $quote Quote. */ public static function send_status_update( $quote ) { $user = get_userdata( $quote->user_id ); if ( ! $user ) { return; } DevisQR_Texts::begin_customer_output(); $mail = self::build_customer_status( $quote, $user->display_name ); DevisQR_Texts::end_customer_output(); $attachments = in_array( $quote->status, array( 'quoted', 'accepted' ), true ) ? self::pdf_attachment( $quote ) : array(); wp_mail( $user->user_email, $mail['subject'], $mail['html'], self::headers(), $attachments ); } /** * Test message (Settings → E-mails): the "request received" template with sample data. * * @param string $to Recipient. * @return bool */ public static function send_test( $to ) { $s = DevisQR_Settings::resolved(); $now = current_time( 'mysql' ); $quote = (object) array( 'id' => 0, 'quote_number' => DevisQR_Settings::get( 'quote_prefix' ) . '-' . gmdate( 'Ym' ) . '-0001', 'status' => 'pending', 'created_at' => $now, 'expires_at' => gmdate( 'Y-m-d H:i:s', strtotime( $now ) + (int) $s['validity_days'] * DAY_IN_SECONDS ), 'total_ht' => 240.00, 'total_tax' => 48.00, 'total_ttc' => 288.00, 'discount_percent' => 0, 'customer_note' => __( 'Sample message left by the customer with the request.', 'nuxwp-devis' ), 'admin_note' => '', 'pdf_path' => '', ); $items = array( (object) array( 'product_name' => __( 'Sample product A', 'nuxwp-devis' ), 'product_sku' => 'SKU-A', 'quantity' => 2, 'line_total_ttc' => 96.00 ), (object) array( 'product_name' => __( 'Sample product B', 'nuxwp-devis' ), 'product_sku' => 'SKU-B', 'quantity' => 1, 'line_total_ttc' => 192.00 ), ); DevisQR_Texts::begin_customer_output(); $mail = self::build_customer_received( $quote, $items, wp_get_current_user()->display_name ); DevisQR_Texts::end_customer_output(); return (bool) wp_mail( $to, '[' . __( 'Test', 'nuxwp-devis' ) . '] ' . $mail['subject'], $mail['html'], self::headers() ); } /* ------------------------------------------------------------------ */ /* Builders (subject + html) */ /* ------------------------------------------------------------------ */ /** * Customer: request received. * * @param object $quote Quote. * @param array $items Items. * @param string $name Customer display name. * @return array {subject, html} */ private static function build_customer_received( $quote, $items, $name ) { $s = DevisQR_Settings::resolved(); $account_url = wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() ); $days = max( 1, (int) $s['validity_days'] ); /* translators: %d: number of days */ $days_text = sprintf( _n( '%d day', '%d days', $days, 'nuxwp-devis' ), $days ); /* translators: 1: quote number, 2: site name */ $subject = sprintf( __( 'Your quote request %1$s — %2$s', 'nuxwp-devis' ), $quote->quote_number, self::site_name() ); $html = self::greeting( $name ) /* translators: %s: quote number */ . self::p( sprintf( esc_html__( 'We have received your quote request %s.', 'nuxwp-devis' ), '' . esc_html( $quote->quote_number ) . '' ) ) . self::p( esc_html__( 'Our team is reviewing your cart and will send you the final quote shortly.', 'nuxwp-devis' ) ) /* translators: %s: duration, e.g. "15 days" */ . self::p( sprintf( esc_html__( 'The quote will remain valid for %s, subject to product availability.', 'nuxwp-devis' ), '' . esc_html( $days_text ) . '' ) ) . self::quote_card( $quote, false ) . self::section_title( __( 'Quote details', 'nuxwp-devis' ) ) . self::items_table( $items ) . self::totals_block( $quote ); if ( ! empty( $quote->customer_note ) ) { $html .= self::note_box( __( 'Your message', 'nuxwp-devis' ), $quote->customer_note ); } $html .= self::p( esc_html__( 'To view your quote and turn it into an order, open the “My quotes” section of your account.', 'nuxwp-devis' ), 'margin-top:20px;' ) . self::button( $account_url, __( 'View my quotes', 'nuxwp-devis' ) ) . self::contact_line( $s ); /* translators: %s: quote number */ $preheader = sprintf( __( 'Your quote request %s has been received.', 'nuxwp-devis' ), $quote->quote_number ); return array( 'subject' => $subject, 'html' => self::wrap( $html, $preheader ) ); } /** * Admin: new request. * * @param object $quote Quote. * @param array $items Items. * @return array {subject, html} */ private static function build_admin_new( $quote, $items ) { $user = get_userdata( $quote->user_id ); $company = $user ? get_user_meta( $user->ID, 'billing_company', true ) : ''; $phone = $user ? get_user_meta( $user->ID, 'billing_phone', true ) : ''; $name = $user ? $user->display_name : __( 'Deleted user', 'nuxwp-devis' ); $admin_url = admin_url( 'admin.php?page=devisqr-quotes#/quote/' . (int) $quote->id ); /* translators: 1: quote number, 2: customer name, 3: total */ $subject = sprintf( __( 'New quote request %1$s — %2$s (%3$s)', 'nuxwp-devis' ), $quote->quote_number, $name, DevisQR_Helpers::price_text( $quote->total_ttc ) ); $rows = array( array( __( 'Customer', 'nuxwp-devis' ), esc_html( $name ) ), ); if ( $company ) { $rows[] = array( __( 'Company', 'nuxwp-devis' ), esc_html( $company ) ); } if ( $user ) { $rows[] = array( __( 'E-mail', 'nuxwp-devis' ), '' . esc_html( $user->user_email ) . '' ); } if ( $phone ) { $rows[] = array( __( 'Phone', 'nuxwp-devis' ), esc_html( $phone ) ); } $html = self::title( __( 'New quote request received', 'nuxwp-devis' ) ) . self::quote_card( $quote, false ) . self::section_title( __( 'Customer information', 'nuxwp-devis' ) ) . self::kv_table( $rows ); if ( ! empty( $quote->customer_note ) ) { $html .= self::note_box( __( 'Customer message', 'nuxwp-devis' ), $quote->customer_note ); } $html .= self::section_title( __( 'Requested products', 'nuxwp-devis' ) ) . self::items_table( $items ) . self::totals_block( $quote ) . self::button( $admin_url, __( 'Process the quote', 'nuxwp-devis' ) ); return array( 'subject' => $subject, 'html' => self::wrap( $html, $subject ) ); } /** * Customer: status changed. * * @param object $quote Quote. * @param string $name Customer display name. * @return array {subject, html} */ private static function build_customer_status( $quote, $name ) { $s = DevisQR_Settings::resolved(); $labels = DevisQR_Helpers::status_labels(); $label = isset( $labels[ $quote->status ] ) ? $labels[ $quote->status ] : $quote->status; $account_url = wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() ); $messages = array( 'pending' => __( 'Your quote is back under review by our team.', 'nuxwp-devis' ), 'quoted' => __( 'Our team has priced your quote. You can review it in your account.', 'nuxwp-devis' ), 'accepted' => __( 'Your quote has been accepted. You can now proceed to payment from your account.', 'nuxwp-devis' ), 'rejected' => __( 'Your quote has been declined. Feel free to contact us for more information.', 'nuxwp-devis' ), 'expired' => __( 'Your quote has expired. You can submit a new quote request at any time.', 'nuxwp-devis' ), 'converted' => __( 'Your quote has been turned into an order. Thank you!', 'nuxwp-devis' ), ); $message = isset( $messages[ $quote->status ] ) ? $messages[ $quote->status ] : $messages['quoted']; /* translators: 1: quote number, 2: status label, 3: site name */ $subject = sprintf( __( 'Quote %1$s — %2$s — %3$s', 'nuxwp-devis' ), $quote->quote_number, $label, self::site_name() ); $cta_text = 'accepted' === $quote->status ? __( 'Accept and pay', 'nuxwp-devis' ) : __( 'View my quote', 'nuxwp-devis' ); $html = self::greeting( $name ) . self::p( esc_html( $message ) ) . self::quote_card( $quote, in_array( $quote->status, array( 'quoted', 'accepted' ), true ), $label ) . self::totals_block( $quote ); if ( ! empty( $quote->admin_note ) ) { $html .= self::note_box( __( 'Message from our team', 'nuxwp-devis' ), $quote->admin_note ); } $html .= self::button( $account_url, $cta_text ) . self::contact_line( $s ); /* translators: 1: quote number, 2: status label */ $preheader = sprintf( __( 'Quote %1$s: %2$s', 'nuxwp-devis' ), $quote->quote_number, $label ); return array( 'subject' => $subject, 'html' => self::wrap( $html, $preheader ) ); } /* ------------------------------------------------------------------ */ /* Layout */ /* ------------------------------------------------------------------ */ /** * Accent colour: cart button background, filterable. * * @return string */ private static function accent() { $style = DevisQR_Settings::get( 'style_cart' ); $accent = ( is_array( $style ) && ! empty( $style['bg'] ) ) ? self::hex( $style['bg'] ) : ''; $accent = $accent ? $accent : '#1e293b'; /** * Filter the accent colour used in e-mails (header line, buttons). * * @param string $accent Hex colour. */ $filtered = self::hex( (string) apply_filters( 'devisqr_email_accent_color', $accent ) ); return $filtered ? $filtered : $accent; } /** * Normalise a hex colour (#abc or #aabbcc) to 6 digits, or '' when invalid. * * @param string $value Colour. * @return string */ private static function hex( $value ) { $value = trim( (string) $value ); if ( preg_match( '/\A#([0-9a-fA-F]{3})\z/', $value, $m ) ) { return '#' . $m[1][0] . $m[1][0] . $m[1][1] . $m[1][1] . $m[1][2] . $m[1][2]; } return preg_match( '/\A#[0-9a-fA-F]{6}\z/', $value ) ? $value : ''; } /** * Decoded site name. * * @return string */ private static function site_name() { return wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); } /** * Logo URL (see DevisQR_Helpers::logo()). * * @return string */ private static function logo_url() { $logo = DevisQR_Helpers::logo(); return $logo ? $logo['url'] : ''; } /** * Page wrapper: logo/name, accent rule, white card, company footer. * * @param string $content Inner HTML. * @param string $preheader Hidden preview text. * @return string */ private static function wrap( $content, $preheader = '' ) { $s = DevisQR_Settings::resolved(); $site_name = self::site_name(); $site_url = home_url(); $accent = self::accent(); $logo = self::logo_url(); $brand = $logo ? '' . esc_attr( $site_name ) . '' : '' . esc_html( $site_name ) . ''; $address = array_filter( array( $s['company_address'], trim( $s['company_postcode'] . ' ' . $s['company_city'] ) ) ); $footer = '

' . esc_html( $s['company_name'] ) . '' . ( $address ? '
' . esc_html( implode( ' · ', $address ) ) : '' ) . ( $s['company_phone'] ? '
' . esc_html( $s['company_phone'] ) : '' ) . '

' . '

' /* translators: %s: site link */ . sprintf( esc_html__( 'This message was sent by %s', 'nuxwp-devis' ), '' . esc_html( $site_name ) . '' ) . '

'; return ' ' . esc_html( $site_name ) . ' ' . ( $preheader ? '
' . esc_html( $preheader ) . '
' : '' ) . '
' . $brand . '
 
' . $content . '
' . $footer . '
'; } /* ------------------------------------------------------------------ */ /* Blocks */ /* ------------------------------------------------------------------ */ /** * Paragraph (inner HTML already escaped). * * @param string $html Content. * @param string $extra Extra CSS. * @return string */ private static function p( $html, $extra = '' ) { return '

' . $html . '

'; } /** * Greeting line. * * @param string $name Customer name. * @return string */ private static function greeting( $name ) { /* translators: %s: customer name */ return self::p( sprintf( esc_html__( 'Hello %s,', 'nuxwp-devis' ), esc_html( $name ) ), 'margin-bottom:16px;' ); } /** * Main title (admin mail). * * @param string $text Title. * @return string */ private static function title( $text ) { return '

' . esc_html( $text ) . '

'; } /** * Section title. * * @param string $text Title. * @return string */ private static function section_title( $text ) { return '

' . esc_html( $text ) . '

'; } /** * Quote card: number · date · (valid until). * * @param object $quote Quote. * @param bool $with_exp Show the expiry date. * @param string $status Status label to show (status e-mail), or ''. * @return string */ private static function quote_card( $quote, $with_exp, $status = '' ) { $cells = array( array( __( 'Quote number', 'nuxwp-devis' ), $quote->quote_number ), ); if ( $status ) { $cells[] = array( __( 'Status', 'nuxwp-devis' ), $status ); } $cells[] = array( __( 'Date', 'nuxwp-devis' ), DevisQR_Helpers::date_text( $quote->created_at ) ); if ( $with_exp && ! empty( $quote->expires_at ) ) { $cells[] = array( __( 'Valid until', 'nuxwp-devis' ), DevisQR_Helpers::date_text( $quote->expires_at ) ); } $html = ''; foreach ( $cells as $i => $cell ) { $html .= ''; } return $html . '

' . esc_html( $cell[0] ) . '

' . esc_html( $cell[1] ) . '

'; } /** * Label / value rows. * * @param array $rows [[label, html], …]. * @return string */ private static function kv_table( $rows ) { $html = ''; foreach ( $rows as $row ) { $html .= ''; } return $html . '
' . esc_html( $row[0] ) . ' ' . $row[1] . '
'; } /** * Items table. * * @param array $items Items. * @return string */ private static function items_table( $items ) { $th = self::FONT . 'font-size:12px;font-weight:700;color:' . self::C_MUTED . ';padding:0 0 8px;border-bottom:1px solid ' . self::C_LINE . ';'; $td = self::FONT . 'font-size:14px;color:' . self::C_TEXT . ';padding:10px 0;border-bottom:1px solid ' . self::C_LINE . ';line-height:1.4;vertical-align:top;'; $html = ''; foreach ( $items as $item ) { $sku = $item->product_sku ? '
' . esc_html__( 'Ref.', 'nuxwp-devis' ) . ' ' . esc_html( $item->product_sku ) . '' : ''; $html .= ''; } return $html . '
' . esc_html__( 'Product', 'nuxwp-devis' ) . ' ' . esc_html__( 'Qty', 'nuxwp-devis' ) . ' ' . esc_html__( 'Total incl. tax', 'nuxwp-devis' ) . '
' . esc_html( $item->product_name ) . $sku . ' ' . esc_html( DevisQR_Helpers::qty_text( $item->quantity ) ) . ' ' . esc_html( DevisQR_Helpers::price_text( $item->line_total_ttc ) ) . '
'; } /** * Totals. * * @param object $quote Quote. * @return string */ private static function totals_block( $quote ) { $label = self::FONT . 'font-size:14px;color:' . self::C_MUTED . ';padding:5px 0;'; $value = self::FONT . 'font-size:14px;color:' . self::C_TEXT . ';padding:5px 0;text-align:right;white-space:nowrap;'; $big = self::FONT . 'font-size:17px;font-weight:700;color:' . self::C_TEXT . ';padding:10px 0 0;border-top:2px solid ' . self::C_TEXT . ';'; $html = ''; if ( ! empty( $quote->discount_percent ) && (float) $quote->discount_percent > 0 ) { $html .= ''; } $html .= '
' . esc_html__( 'Global discount', 'nuxwp-devis' ) . '−' . esc_html( DevisQR_Helpers::percent_text( $quote->discount_percent ) ) . '
' . esc_html__( 'Total excl. tax', 'nuxwp-devis' ) . '' . esc_html( DevisQR_Helpers::price_text( $quote->total_ht ) ) . '
' . esc_html__( 'Tax', 'nuxwp-devis' ) . '' . esc_html( DevisQR_Helpers::price_text( $quote->total_tax ) ) . '
' . esc_html__( 'Total incl. tax', 'nuxwp-devis' ) . '' . esc_html( DevisQR_Helpers::price_text( $quote->total_ttc ) ) . '
 
'; return $html; } /** * Quoted note (customer message / team message). * * @param string $label Label. * @param string $text Raw text. * @return string */ private static function note_box( $label, $text ) { return '

' . esc_html( $label ) . '

' . nl2br( esc_html( $text ) ) . '

'; } /** * Button. * * @param string $url URL. * @param string $text Label. * @return string */ private static function button( $url, $text ) { $accent = self::accent(); return '
' . esc_html( $text ) . '
'; } /** * Contact line at the end of customer mails. * * @param array $s Resolved settings. * @return string */ private static function contact_line( $s ) { $link = '' . esc_html( $s['contact_email'] ) . ''; /* translators: %s: contact e-mail link */ return '

' . sprintf( esc_html__( 'Any question? Contact us at %s', 'nuxwp-devis' ), $link ) . '

'; } /* ------------------------------------------------------------------ */ /* Transport */ /* ------------------------------------------------------------------ */ /** * Headers for customer e-mails. * * @return array */ private static function headers() { $s = DevisQR_Settings::resolved(); $name = str_replace( array( '<', '>', '"', "\r", "\n" ), '', self::site_name() ); // wp_mail() splits the From header on '<'. return array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . trim( $name ) . ' <' . $s['from_email'] . '>', 'Reply-To: ' . $s['contact_email'], ); } /** * Attachments (PDF if present). * * @param object $quote Quote. * @return array */ private static function pdf_attachment( $quote ) { if ( empty( $quote->pdf_path ) ) { return array(); } $path = DevisQR_Database::resolve_storage_path( $quote->pdf_path, 'pdf' ); return $path ? array( $path ) : array(); } }