diff --git a/CRM/Civicase/BAO/CaseCategoryFeatures.php b/CRM/Civicase/BAO/CaseCategoryFeatures.php new file mode 100644 index 000000000..a23f7cc32 --- /dev/null +++ b/CRM/Civicase/BAO/CaseCategoryFeatures.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/BAO/CaseSalesOrder.php b/CRM/Civicase/BAO/CaseSalesOrder.php new file mode 100644 index 000000000..8da5376c0 --- /dev/null +++ b/CRM/Civicase/BAO/CaseSalesOrder.php @@ -0,0 +1,90 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + + /** + * Computes the sales order line item total. + * + * @param array $items + * Array of sales order line items. + * + * @return array + * ['totalAfterTax' => , 'totalBeforeTax' => ] + */ + public static function computeTotal(array $items) { + $totalBeforeTax = round(array_reduce($items, fn ($a, $b) => $a + self::getSubTotal($b), 0), 2); + $totalAfterTax = round(array_reduce($items, + fn ($a, $b) => $a + (($b['tax_rate'] * self::getSubTotal($b)) / 100), + 0 + ) + $totalBeforeTax, 2); + + return [ + 'taxRates' => self::computeTaxRates($items), + 'totalAfterTax' => $totalAfterTax, + 'totalBeforeTax' => $totalBeforeTax, + ]; + } + + /** + * Computes the sub total of a single line item. + * + * @param array $item + * Single sales order line item. + * + * @return int + * The line item subtotal. + */ + public static function getSubTotal(array $item) { + return $item['unit_price'] * $item['quantity'] * ((100 - ($item['discounted_percentage'] ?? 0)) / 100) ?? 0; + } + + /** + * Computes the tax rates of each line item. + * + * @param array $items + * Single sales order line item. + * + * @return array + * Returned sorted array of line items tax rates. + */ + public static function computeTaxRates(array $items) { + $items = array_filter($items, fn ($a) => $a['tax_rate'] > 0); + usort($items, fn ($a, $b) => $a['tax_rate'] <=> $b['tax_rate']); + + return array_map( + fn ($a) => + [ + 'rate' => round($a['tax_rate'], 2), + 'value' => round(($a['tax_rate'] * self::getSubTotal($a)) / 100, 2), + ], + $items + ); + } + +} diff --git a/CRM/Civicase/BAO/CaseSalesOrderLine.php b/CRM/Civicase/BAO/CaseSalesOrderLine.php new file mode 100644 index 000000000..c48676d53 --- /dev/null +++ b/CRM/Civicase/BAO/CaseSalesOrderLine.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/DAO/CaseCategoryFeatures.php b/CRM/Civicase/DAO/CaseCategoryFeatures.php new file mode 100644 index 000000000..35e33a992 --- /dev/null +++ b/CRM/Civicase/DAO/CaseCategoryFeatures.php @@ -0,0 +1,208 @@ +__table = 'civicrm_case_category_features'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Category Featureses') : E::ts('Case Category Features'); + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseCategoryFeatures ID'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'category_id' => [ + 'name' => 'category_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_type_categories option group'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.category_id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'case_type_categories', + 'optionEditPath' => 'civicrm/admin/options/case_type_categories', + ], + 'add' => NULL, + ], + 'feature_id' => [ + 'name' => 'feature_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_type_category_features option group'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.feature_id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'case_type_category_features', + 'optionEditPath' => 'civicrm/admin/options/case_type_category_features', + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_category_features', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_category_features', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/CRM/Civicase/DAO/CaseCategoryInstance.php b/CRM/Civicase/DAO/CaseCategoryInstance.php index c087599b0..39cecd4f5 100644 --- a/CRM/Civicase/DAO/CaseCategoryInstance.php +++ b/CRM/Civicase/DAO/CaseCategoryInstance.php @@ -4,15 +4,18 @@ * @package CRM * @copyright CiviCRM LLC https://civicrm.org/licensing * - * Generated from /var/www/site2/profiles/compuclient/modules/contrib/civicrm/ext/uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseCategoryInstance.xml + * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseCategoryInstance.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:b15b25c6edb87d09a297cc7861c72b27) + * (GenCodeChecksum:c54be94451d9f9473afcc991619787f0) */ +use CRM_Civicase_ExtensionUtil as E; /** * Database access object for the CaseCategoryInstance entity. */ class CRM_Civicase_DAO_CaseCategoryInstance extends CRM_Core_DAO { + const EXT = E::LONG_NAME; + const TABLE_ADDED = ''; /** * Static instance to hold the table name. @@ -31,21 +34,27 @@ class CRM_Civicase_DAO_CaseCategoryInstance extends CRM_Core_DAO { /** * Unique CaseCategoryInstance Id * - * @var int + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $id; /** * One of the values of the case_type_categories option group * - * @var int + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $category_id; /** * One of the values of the case_category_instance_type option group * - * @var int + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $instance_id; @@ -57,6 +66,16 @@ public function __construct() { parent::__construct(); } + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Category Instances') : E::ts('Case Category Instance'); + } + /** * Returns all the column names of this table * @@ -68,18 +87,20 @@ public static function &fields() { 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('Unique CaseCategoryInstance Id'), + 'description' => E::ts('Unique CaseCategoryInstance Id'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.id', 'table_name' => 'civicrm_case_category_instance', 'entity' => 'CaseCategoryInstance', 'bao' => 'CRM_Civicase_DAO_CaseCategoryInstance', 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, ], 'category_id' => [ 'name' => 'category_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('One of the values of the case_type_categories option group'), + 'description' => E::ts('One of the values of the case_type_categories option group'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.category_id', 'table_name' => 'civicrm_case_category_instance', @@ -90,11 +111,12 @@ public static function &fields() { 'optionGroupName' => 'case_type_categories', 'optionEditPath' => 'civicrm/admin/options/case_type_categories', ], + 'add' => NULL, ], 'instance_id' => [ 'name' => 'instance_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('One of the values of the case_category_instance_type option group'), + 'description' => E::ts('One of the values of the case_category_instance_type option group'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.instance_id', 'table_name' => 'civicrm_case_category_instance', @@ -105,6 +127,7 @@ public static function &fields() { 'optionGroupName' => 'case_category_instance_type', 'optionEditPath' => 'civicrm/admin/options/case_category_instance_type', ], + 'add' => NULL, ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); diff --git a/CRM/Civicase/DAO/CaseContactLock.php b/CRM/Civicase/DAO/CaseContactLock.php index 12e441640..057de4e57 100644 --- a/CRM/Civicase/DAO/CaseContactLock.php +++ b/CRM/Civicase/DAO/CaseContactLock.php @@ -2,50 +2,59 @@ /** * @package CRM - * @copyright CiviCRM LLC (c) 2004-2017 + * @copyright CiviCRM LLC https://civicrm.org/licensing * - * Generated from xml/schema/CRM/Civicase/CaseContactLock.xml + * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseContactLock.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:db5bf4a479b00ab2f957675c14fbabcc) + * (GenCodeChecksum:df5738a7e2ec72d54c3a5834697213f6) */ +use CRM_Civicase_ExtensionUtil as E; /** * Database access object for the CaseContactLock entity. */ class CRM_Civicase_DAO_CaseContactLock extends CRM_Core_DAO { + const EXT = E::LONG_NAME; + const TABLE_ADDED = '4.7'; /** * Static instance to hold the table name. * * @var string */ - static $_tableName = 'civicase_contactlock'; + public static $_tableName = 'civicase_contactlock'; /** * Should CiviCRM log any modifications to this table in the civicrm_log table. * * @var bool */ - static $_log = TRUE; + public static $_log = TRUE; /** * Unique CaseContactLock ID * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $id; /** * Case ID that is locked. * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $case_id; /** * Contact for which the case is locked. * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $contact_id; @@ -57,6 +66,16 @@ public function __construct() { parent::__construct(); } + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Contact Locks') : E::ts('Case Contact Lock'); + } + /** * Returns foreign keys and entity references. * @@ -65,7 +84,7 @@ public function __construct() { */ public static function getReferenceColumns() { if (!isset(Civi::$statics[__CLASS__]['links'])) { - Civi::$statics[__CLASS__]['links'] = static ::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_id', 'civicrm_case', 'id'); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id'); CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); @@ -84,32 +103,39 @@ public static function &fields() { 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Unique CaseContactLock ID', + 'description' => E::ts('Unique CaseContactLock ID'), 'required' => TRUE, + 'where' => 'civicase_contactlock.id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, + 'readonly' => TRUE, + 'add' => '4.4', ], 'case_id' => [ 'name' => 'case_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Case ID that is locked.', + 'description' => E::ts('Case ID that is locked.'), + 'where' => 'civicase_contactlock.case_id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, 'FKClassName' => 'CRM_Case_DAO_Case', + 'add' => '4.7', ], 'contact_id' => [ 'name' => 'contact_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Contact for which the case is locked.', + 'description' => E::ts('Contact for which the case is locked.'), + 'where' => 'civicase_contactlock.contact_id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'add' => '4.7', ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php new file mode 100644 index 000000000..9e2304e13 --- /dev/null +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -0,0 +1,561 @@ + 'civicrm/case-features/quotations/view?reset=1&id=[id]', + 'update' => 'civicrm/case-features/a#/quotations/new?reset=1&id=[id]', + 'delete' => 'civicrm/case-features/quotations/delete?reset=1&id=[id]', + ]; + + /** + * Unique CaseSalesOrder ID + * + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $id; + + /** + * FK to Contact + * + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $client_id; + + /** + * FK to Contact + * + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $owner_id; + + /** + * FK to Case + * + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $case_id; + + /** + * 3 character string, value from config setting or input via user. + * + * @var string|null + * (SQL type: varchar(3)) + * Note that values will be retrieved from the database as a string. + */ + public $currency; + + /** + * One of the values of the case_sales_order_status option group + * + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $status_id; + + /** + * One of the values of the case_sales_order_invoicing_status option group + * + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $invoicing_status_id; + + /** + * One of the values of the case_sales_order_payment_status option group + * + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $payment_status_id; + + /** + * Sales order deesctiption + * + * @var string + * (SQL type: text) + * Note that values will be retrieved from the database as a string. + */ + public $description; + + /** + * Sales order notes + * + * @var string + * (SQL type: text) + * Note that values will be retrieved from the database as a string. + */ + public $notes; + + /** + * Total amount of the sales order line items before tax deduction. + * + * @var float|string + * (SQL type: decimal(20,2)) + * Note that values will be retrieved from the database as a string. + */ + public $total_before_tax; + + /** + * Total amount of the sales order line items after tax deduction. + * + * @var float|string + * (SQL type: decimal(20,2)) + * Note that values will be retrieved from the database as a string. + */ + public $total_after_tax; + + /** + * Quotation date + * + * @var string|null + * (SQL type: timestamp) + * Note that values will be retrieved from the database as a string. + */ + public $quotation_date; + + /** + * Date the sales order is created + * + * @var string|null + * (SQL type: timestamp) + * Note that values will be retrieved from the database as a string. + */ + public $created_at; + + /** + * Is this sales order deleted? + * + * @var bool|string|null + * (SQL type: tinyint) + * Note that values will be retrieved from the database as a string. + */ + public $is_deleted; + + /** + * Class constructor. + */ + public function __construct() { + $this->__table = 'civicase_sales_order'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Quotations') : E::ts('Quotation'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'client_id', 'civicrm_contact', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'owner_id', 'civicrm_contact', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_id', 'civicrm_case', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseSalesOrder ID'), + 'required' => TRUE, + 'where' => 'civicase_sales_order.id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'client_id' => [ + 'name' => 'client_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Contact'), + 'where' => 'civicase_sales_order.client_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Client"), + ], + 'add' => NULL, + ], + 'owner_id' => [ + 'name' => 'owner_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Contact'), + 'where' => 'civicase_sales_order.owner_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Owner"), + ], + 'add' => NULL, + ], + 'case_id' => [ + 'name' => 'case_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Case'), + 'where' => 'civicase_sales_order.case_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Case_DAO_Case', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Case/Opportunity"), + ], + 'add' => NULL, + ], + 'currency' => [ + 'name' => 'currency', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Financial Currency'), + 'description' => E::ts('3 character string, value from config setting or input via user.'), + 'maxlength' => 3, + 'size' => CRM_Utils_Type::FOUR, + 'where' => 'civicase_sales_order.currency', + 'headerPattern' => '/cur(rency)?/i', + 'dataPattern' => '/^[A-Z]{3}$/', + 'default' => NULL, + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + ], + 'pseudoconstant' => [ + 'table' => 'civicrm_currency', + 'keyColumn' => 'name', + 'labelColumn' => 'full_name', + 'nameColumn' => 'name', + 'abbrColumn' => 'symbol', + ], + 'add' => NULL, + ], + 'status_id' => [ + 'name' => 'status_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_sales_order_status option group'), + 'required' => TRUE, + 'where' => 'civicase_sales_order.status_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + 'label' => E::ts("Status"), + ], + 'pseudoconstant' => [ + 'optionGroupName' => 'case_sales_order_status', + 'optionEditPath' => 'civicrm/admin/options/case_sales_order_status', + ], + 'add' => NULL, + ], + 'invoicing_status_id' => [ + 'name' => 'invoicing_status_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_sales_order_invoicing_status option group'), + 'required' => TRUE, + 'where' => 'civicase_sales_order.invoicing_status_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + 'label' => E::ts("Invoicing"), + ], + 'pseudoconstant' => [ + 'optionGroupName' => 'case_sales_order_invoicing_status', + 'optionEditPath' => 'civicrm/admin/options/case_sales_order_invoicing_status', + ], + 'add' => NULL, + ], + 'payment_status_id' => [ + 'name' => 'payment_status_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_sales_order_payment_status option group'), + 'required' => TRUE, + 'where' => 'civicase_sales_order.payment_status_id', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + 'label' => E::ts("Payments"), + ], + 'pseudoconstant' => [ + 'optionGroupName' => 'case_sales_order_payment_status', + 'optionEditPath' => 'civicrm/admin/options/case_sales_order_payment_status', + ], + 'add' => NULL, + ], + 'description' => [ + 'name' => 'description', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Description'), + 'description' => E::ts('Sales order deesctiption'), + 'required' => FALSE, + 'where' => 'civicase_sales_order.description', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'TextArea', + 'label' => E::ts("Description"), + ], + 'add' => NULL, + ], + 'notes' => [ + 'name' => 'notes', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Notes'), + 'description' => E::ts('Sales order notes'), + 'required' => FALSE, + 'where' => 'civicase_sales_order.notes', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'RichTextEditor', + 'label' => E::ts("Notes"), + ], + 'add' => NULL, + ], + 'total_before_tax' => [ + 'name' => 'total_before_tax', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Total Before Tax'), + 'description' => E::ts('Total amount of the sales order line items before tax deduction.'), + 'required' => FALSE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order.total_before_tax', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + ], + 'add' => NULL, + ], + 'total_after_tax' => [ + 'name' => 'total_after_tax', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Total After Tax'), + 'description' => E::ts('Total amount of the sales order line items after tax deduction.'), + 'required' => FALSE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order.total_after_tax', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + ], + 'add' => NULL, + ], + 'quotation_date' => [ + 'name' => 'quotation_date', + 'type' => CRM_Utils_Type::T_TIMESTAMP, + 'title' => E::ts('Quotation Date'), + 'description' => E::ts('Quotation date'), + 'where' => 'civicase_sales_order.quotation_date', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select Date', + ], + 'add' => NULL, + ], + 'created_at' => [ + 'name' => 'created_at', + 'type' => CRM_Utils_Type::T_TIMESTAMP, + 'title' => E::ts('Created At'), + 'description' => E::ts('Date the sales order is created'), + 'where' => 'civicase_sales_order.created_at', + 'default' => 'CURRENT_TIMESTAMP', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_deleted' => [ + 'name' => 'is_deleted', + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'description' => E::ts('Is this sales order deleted?'), + 'where' => 'civicase_sales_order.is_deleted', + 'default' => '0', + 'table_name' => 'civicase_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, '_sales_order', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, '_sales_order', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/CRM/Civicase/DAO/CaseSalesOrderLine.php b/CRM/Civicase/DAO/CaseSalesOrderLine.php new file mode 100644 index 000000000..a70bf5f84 --- /dev/null +++ b/CRM/Civicase/DAO/CaseSalesOrderLine.php @@ -0,0 +1,418 @@ +__table = 'civicase_sales_order_line'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Quotation Lines') : E::ts('Quotation Line'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'sales_order_id', 'civicase_sales_order', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'financial_type_id', 'civicrm_financial_type', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'product_id', 'civicrm_product', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseSalesOrderLine ID'), + 'required' => TRUE, + 'where' => 'civicase_sales_order_line.id', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'sales_order_id' => [ + 'name' => 'sales_order_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to CaseSalesOrder'), + 'where' => 'civicase_sales_order_line.sales_order_id', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'html' => [ + 'type' => 'EntityRef', + ], + 'add' => NULL, + ], + 'financial_type_id' => [ + 'name' => 'financial_type_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to CiviCRM Financial Type'), + 'where' => 'civicase_sales_order_line.financial_type_id', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Financial_DAO_FinancialType', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Financial Type"), + ], + 'add' => NULL, + ], + 'product_id' => [ + 'name' => 'product_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Product ID'), + 'where' => 'civicase_sales_order_line.product_id', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contribute_DAO_Product', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Product"), + ], + 'add' => NULL, + ], + 'item_description' => [ + 'name' => 'item_description', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Item Description'), + 'description' => E::ts('line item deesctiption'), + 'required' => FALSE, + 'where' => 'civicase_sales_order_line.item_description', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'TextArea', + 'label' => E::ts("Item Description"), + ], + 'add' => NULL, + ], + 'quantity' => [ + 'name' => 'quantity', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Quantity'), + 'description' => E::ts('Quantity'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_line.quantity', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Quantity"), + ], + 'add' => NULL, + ], + 'unit_price' => [ + 'name' => 'unit_price', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Unit Price'), + 'description' => E::ts('Unit Price'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_line.unit_price', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Unit Price"), + ], + 'add' => NULL, + ], + 'tax_rate' => [ + 'name' => 'tax_rate', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Tax Rate'), + 'description' => E::ts('Tax rate for the line item'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_line.tax_rate', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Tax"), + ], + 'add' => NULL, + ], + 'discounted_percentage' => [ + 'name' => 'discounted_percentage', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Discounted Percentage'), + 'description' => E::ts('Discount applied to the line item'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_line.discounted_percentage', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Discount"), + ], + 'add' => NULL, + ], + 'subtotal_amount' => [ + 'name' => 'subtotal_amount', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Subtotal Amount'), + 'description' => E::ts('Quantity x Unit Price x (100-Discount)%'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_line.subtotal_amount', + 'table_name' => 'civicase_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Subtotal"), + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, '_sales_order_line', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, '_sales_order_line', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php new file mode 100644 index 000000000..6261a825b --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -0,0 +1,237 @@ +id = CRM_Utils_Request::retrieve('id', 'Positive', $this); + } + + /** + * {@inheritDoc} + */ + public function buildQuickForm() { + $this->addElement('radio', 'to_be_invoiced', '', ts('Enter % to be invoiced ?'), + self::INVOICE_PERCENT, [ + 'id' => 'invoice_percent', + ]); + $this->add('text', 'percent_value', '', [ + 'id' => 'percent_value', + 'placeholder' => 'Percentage to be invoiced', + 'class' => 'form-control', + 'min' => 1, + ], FALSE); + + if ($this->hasRemainingBalance()) { + $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), + self::INVOICE_REMAIN, + ['id' => 'invoice_remain'] + ); + $this->addRule('to_be_invoiced', ts('Invoice value is required'), 'required'); + } + + $statusOptions = OptionValue::get() + ->addSelect('value', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute() + ->getArrayCopy(); + + $this->add( + 'select', + 'status', + ts('Update status of quotation to'), + ['' => 'Select'] + + array_combine( + array_column($statusOptions, 'value'), + array_column($statusOptions, 'label') + ), + TRUE, + ['class' => 'form-control'] + ); + + $this->addButtons([ + [ + 'type' => 'submit', + 'name' => E::ts('Create Contribution'), + ], + [ + 'type' => 'cancel', + 'name' => E::ts('Cancel'), + // 'isDefault' => TRUE, + ], + ]); + + parent::buildQuickForm(); + } + + /** + * {@inheritDoc} + */ + public function setDefaultValues() { + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $this->id) + ->addSelect('status_id') + ->execute() + ->first(); + + return [ + 'status' => $caseSalesOrder['status_id'] ?? NULL, + ]; + } + + /** + * {@inheritDoc} + */ + public function addRules() { + $this->addFormRule([$this, 'formRule']); + $this->addFormRule([$this, 'validateAmount']); + } + + /** + * Form Validation rule. + * + * This enforces the rule whereby, + * user must supply an amount if the + * enter percentage amount radio is selected. + * + * @param array $values + * Array of submitted values. + * + * @return array|bool + * Returns the form errors if form is invalid + */ + public function formRule(array $values) { + $errors = []; + + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && empty(floatval($values['percent_value']))) { + $errors['percent_value'] = 'Percentage value is required'; + } + + return $errors ?: TRUE; + } + + /** + * Validate Invoice value. + * + * Ensures that percent amount entered by user or + * calculated as part of other remaining balance + * selection is correct and not exceeding the + * balance amount. + * + * e.g. If a sales_order total_amount is 1000, + * and has the following contributions + * contribution 1 with value - 500 + * contribution 2 with value - 250 + * the amount owed is 250, so any new contribution + * that will exceed this amount should return an error. + * + * @param array $values + * Array of submitted values. + * + * @return array|bool + * Returns the form errors if form is invalid + */ + public function validateAmount(array $values) { + $errors = []; + + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT) { + return TRUE; + } + + if (!$this->hasRemainingBalance()) { + $errors['to_be_invoiced'] = 'Unable to create a contribution due to insufficient balance.'; + } + + return $errors ?: TRUE; + } + + /** + * Checks if the sales order has left over balance to be invoiced. + */ + public function hasRemainingBalance() { + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('total_after_tax') + ->addWhere('id', '=', $this->id) + ->setLimit(1) + ->execute() + ->first(); + if (empty($caseSalesOrder)) { + throw new CRM_Core_Exception("The specified case sales order doesn't exist"); + } + + // Get all the previous contributions. + $contributions = Contribution::get() + ->addSelect('total_amount') + ->addWhere('Opportunity_Details.Quotation', '=', $this->id) + ->execute() + ->jsonSerialize(); + + $paidTotal = array_sum(array_column($contributions, 'total_amount')); + $remainBalance = $caseSalesOrder['total_after_tax'] - $paidTotal; + $remainBalance = round($remainBalance, 2); + + if ($remainBalance <= 0) { + return FALSE; + } + + return TRUE; + } + + /** + * {@inheritDoc} + */ + public function postProcess() { + $values = $this->getSubmitValues(); + + if (!empty($this->id) && !empty($values['to_be_invoiced'])) { + $this->createContribution($values); + } + } + + /** + * Redirects user to contribution add page. + * + * This contribution page will have the line items + * prefilled from the sales order line items. + */ + public function createContribution(array $values) { + $query = [ + 'action' => 'add', + 'reset' => 1, + 'context' => 'standalone', + 'sales_order' => $this->id, + 'sales_order_status_id' => $values['status'], + 'to_be_invoiced' => $values['to_be_invoiced'], + 'percent_value' => $values['to_be_invoiced'] == + self::INVOICE_PERCENT ? floatval($values['percent_value']) : 0, + ]; + + $url = CRM_Utils_System::url('civicrm/contribute/add', $query); + CRM_Utils_System::redirect($url); + } + +} diff --git a/CRM/Civicase/Form/CaseSalesOrderDelete.php b/CRM/Civicase/Form/CaseSalesOrderDelete.php new file mode 100755 index 000000000..7922e23db --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderDelete.php @@ -0,0 +1,62 @@ +id = CRM_Utils_Request::retrieve('id', 'Positive', $this); + } + + /** + * {@inheritDoc} + */ + public function buildQuickForm() { + $this->assign('id', $this->id); + + $this->addButtons([ + [ + 'type' => 'submit', + 'name' => E::ts('Delete'), + ], + [ + 'type' => 'cancel', + 'name' => E::ts('Cancel'), + 'isDefault' => TRUE, + ], + ]); + + parent::buildQuickForm(); + } + + /** + * {@inheritDoc} + */ + public function postProcess() { + if (!empty($this->id)) { + CaseSalesOrder::delete() + ->addWhere('id', '=', $this->id) + ->execute(); + CRM_Core_Session::setStatus(E::ts('Quotation is deleted successfully.'), ts('Quotation deleted'), 'success'); + } + } + +} diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php new file mode 100644 index 000000000..9315f99e6 --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -0,0 +1,191 @@ +setTitle('Email Quotation'); + $this->salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + + $this->setContactIDs(); + $this->setIsSearchContext(FALSE); + $this->traitPreProcess(); + } + + /** + * List available tokens for this form. + * + * Presently all tokens are returned. + * + * @return array + * List of Available tokens + * + * @throws \CRM_Core_Exception + */ + public function listTokens() { + $tokenProcessor = new TokenProcessor(Civi::dispatcher(), ['schema' => ['contactId']]); + $tokens = $tokenProcessor->listTokens(); + + return $tokens; + } + + /** + * Submit the form values. + * + * This is also accessible for testing. + * + * @param array $formValues + * Submitted values. + * + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException + * @throws \API_Exception + */ + public function submit(array $formValues): void { + $this->saveMessageTemplate($formValues); + $sents = 0; + $from = $formValues['from_email_address']; + $text = $this->getSubmittedValue('text_message'); + $html = $this->getSubmittedValue('html_message'); + $from = CRM_Utils_Mail::formatFromAddress($from); + + $cc = $this->getCc(); + $additionalDetails = empty($cc) ? '' : "\ncc : " . $this->getEmailUrlString($this->getCcArray()); + + $bcc = $this->getBcc(); + $additionalDetails .= empty($bcc) ? '' : "\nbcc : " . $this->getEmailUrlString($this->getBccArray()); + + $quotationInvoice = self::getQuotationInvoice(); + + foreach ($this->getRowsForEmails() as $values) { + $mailParams = []; + $mailParams['messageTemplate'] = [ + 'msg_text' => $text, + 'msg_html' => $html, + 'msg_subject' => $this->getSubject(), + ]; + $mailParams['tokenContext'] = [ + 'contactId' => $values['contact_id'], + 'salesOrderId' => $this->salesOrderId, + ]; + $mailParams['tplParams'] = []; + $mailParams['from'] = $from; + $mailParams['toEmail'] = $values['email']; + $mailParams['cc'] = $cc ?? NULL; + $mailParams['bcc'] = $bcc ?? NULL; + $mailParams['attachments'][] = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $quotationInvoice['html'], $quotationInvoice['format']); + // Send the mail. + [$sent, $subject, $message, $html] = CRM_Core_BAO_MessageTemplate::sendTemplate($mailParams); + $sents += ($sent ? 1 : 0); + } + + CRM_Core_Session::setStatus(ts('One email has been sent successfully. ', [ + 'plural' => '%count emails were sent successfully. ', + 'count' => $sents, + ]), ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $sents]), 'success'); + + } + + /** + * {@inheritDoc} + */ + public function setContactIDs() { // phpcs:ignore + $this->_contactIds = $this->getContactIds(); + } + + /** + * Returns Sales Order Client Contact ID. + * + * @return array + * Client Contact ID as an array + */ + protected function getContactIds(): array { + if (isset($this->_contactIds)) { + return $this->_contactIds; + } + + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $this->_contactIds = [$caseSalesOrder['client_id']]; + + return $this->_contactIds; + } + + /** + * Renders the quotatioin invoice message template. + * + * @return array + * Rendered message, consistent of 'subject', 'text', 'html' + */ + public static function getQuotationInvoice(): array { + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + + /** @var \CRM_Civicase_Service_CaseSalesOrderInvoice */ + $invoiceService = new \CRM_Civicase_Service_CaseSalesOrderInvoice(new \CRM_Civicase_WorkflowMessage_SalesOrderInvoice()); + return $invoiceService->render($salesOrderId); + } + + /** + * Get the rows for each contactID. + * + * @return array + * Array if contact IDs. + */ + protected function getRows(): array { + $rows = []; + foreach ($this->_contactIds as $index => $contactID) { + $rows[] = [ + 'contact_id' => $contactID, + 'schema' => ['contactId' => $contactID], + ]; + } + return $rows; + } + + /** + * Renders and return the generated PDF to the browser. + */ + public static function download(): void { + $rendered = self::getQuotationInvoice(); + ob_end_clean(); + CRM_Utils_PDF_Utils::html2pdf($rendered['html'], 'quotation_invoice.pdf', FALSE, $rendered['format']); + CRM_Utils_System::civiExit(); + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php new file mode 100644 index 000000000..f77b33f36 --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php @@ -0,0 +1,99 @@ +shouldRun($form, $formName)) { + return; + } + + $this->addCategoryFeaturesFormField($form); + $this->addCategoryFeaturesTemplate(); + } + + /** + * Adds the Case Category Features Form field. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + private function addCategoryFeaturesFormField(CRM_Core_Form &$form) { + $caseCategoryFeatures = new CRM_Civicase_Service_CaseTypeCategoryFeatures(); + $features = []; + + foreach ($caseCategoryFeatures->getFeatures() as $feature) { + $features[] = 'case_category_feature_' . $feature['value']; + $form->add( + 'checkbox', + 'case_category_feature_' . $feature['value'], + $feature['label'] + ); + } + + $form->assign('features', $features); + $this->setDefaultValues($form); + } + + /** + * Adds the template for case category features field template. + */ + private function addCategoryFeaturesTemplate() { + $templatePath = CRM_Civicase_ExtensionUtil::path() . '/templates'; + CRM_Core_Region::instance('page-body')->add( + [ + 'template' => "{$templatePath}/CRM/Civicase/Form/CaseCategoryFeatures.tpl", + ] + ); + } + + /** + * Sets default values. + */ + private function setDefaultValues(CRM_Core_Form &$form) { + if (empty($form->getVar('_id'))) { + return; + } + + $defaults = $form->_defaultValues; + $defaultFeatures = $this->getDefaultFeatures($form); + $form->setDefaults(array_merge($defaults, $defaultFeatures)); + } + + /** + * Returns the default value for the category instance fields. + * + * @param CRM_Core_Form $form + * Form Class object. + * + * @return mixed|null + * Default value. + */ + private function getDefaultFeatures(CRM_Core_Form $form) { + $caseCategory = $form->getVar('_values')['value']; + $enabledFeatures = []; + + $caseCategoryFeatures = CaseCategoryFeatures::get() + ->addWhere('category_id', '=', $caseCategory) + ->execute(); + + foreach ($caseCategoryFeatures as $caseCategoryFeature) { + $enabledFeatures['case_category_feature_' . $caseCategoryFeature['feature_id']] = 1; + } + + return $enabledFeatures; + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php new file mode 100644 index 000000000..9e6b6c53d --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php @@ -0,0 +1,85 @@ +shouldRun($form, $formName)) { + return; + } + + $customFields['case'] = [ + 'name' => CRM_Core_BAO_CustomField::getCustomFieldID('Case_Opportunity', 'Opportunity_Details', TRUE), + 'entity' => 'Case', + 'placeholder' => '- Select Case/Opportunity -', + ]; + + $customFields['salesOrder'] = [ + 'name' => CRM_Core_BAO_CustomField::getCustomFieldID('Quotation', 'Opportunity_Details', TRUE), + 'entity' => 'CaseSalesOrder', + 'placeholder' => '- Select Quotation -', + ]; + + \Civi::resources()->add([ + 'scriptFile' => [E::LONG_NAME, 'js/contribution-entityref-field.js'], + 'region' => 'page-header', + ]); + + $this->populateDefaultFields($form, $customFields); + \Civi::resources()->addVars('civicase', ['entityRefCustomFields' => $customFields]); + } + + /** + * Populates default fields. + * + * @param \CRM_Core_Form &$form + * Form Class object. + * @param array &$customFields + * Custom fields to set default value. + */ + private function populateDefaultFields(CRM_Core_Form &$form, array &$customFields) { + $caseId = CRM_Utils_Request::retrieve('caseId', 'Positive', $form); + if (!$caseId) { + return; + } + + $customFields['case']['value'] = $caseId; + $caseClient = CaseContact::get() + ->addSelect('contact_id') + ->addWhere('case_id', '=', $caseId) + ->execute() + ->first()['contact_id'] ?? NULL; + + $form->setDefaults(array_merge($form->_defaultValues, ['contact_id' => $caseClient])); + } + + /** + * Checks if the hook should run. + * + * @param \CRM_Core_Form $form + * Form object. + * @param string $formName + * Form Name. + * + * @return bool + * True if hook should run, otherwise false. + */ + public function shouldRun($form, $formName) { + $addOrUpdate = ($form->getAction() & CRM_Core_Action::ADD) || ($form->getAction() & CRM_Core_Action::UPDATE); + return $formName === "CRM_Contribute_Form_Contribution" && $addOrUpdate; + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php new file mode 100644 index 000000000..f7d243b98 --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php @@ -0,0 +1,65 @@ +shouldRun($formName)) { + return; + } + + $this->addQuotationsNoteField($form); + } + + /** + * Checks if this shook should run. + * + * @param string $formName + * Form Name. + * + * @return bool + * True if the hook should run. + */ + public function shouldRun($formName) { + return $formName == CRM_Admin_Form_Preferences_Contribute::class; + } + + /** + * Add Quotations note fields. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + public function addQuotationsNoteField(CRM_Core_Form &$form) { + $fieldName = 'quotations_notes'; + $field = [ + $fieldName => [ + 'html_type' => 'wysiwyg', + 'title' => ts('Terms/Notes for Quotations'), + 'weight' => 5, + 'description' => ts('Enter note or message to be displyaed on quotations'), + 'attributes' => ['rows' => 2, 'cols' => 40], + ], + ]; + + $form->add('wysiwyg', $fieldName, $field[$fieldName]['title'], $field[$fieldName]['attributes']); + $form->assign('htmlFields', array_merge($form->get_template_vars('htmlFields'), $field)); + $value = Civi::settings()->get($fieldName) ?? NULL; + $form->setDefaults(array_merge($form->_defaultValues, [$fieldName => $value])); + + CRM_Core_Region::instance('form-buttons')->add([ + 'template' => "CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl", + ]); + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php new file mode 100644 index 000000000..26c5b1974 --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php @@ -0,0 +1,64 @@ +shouldRun($form, $formName, $salesOrderId)) { + return; + } + $lineItemGenerator = new salesOrderlineItemGenerator($salesOrderId, $toBeInvoiced, $percentValue); + $lineItems = $lineItemGenerator->generateLineItems(); + + CRM_Core_Resources::singleton() + ->addScriptFile(E::LONG_NAME, 'js/sales-order-contribution.js') + ->addVars(E::LONG_NAME, [ + 'sales_order' => $salesOrderId, + 'sales_order_status_id' => $status, + 'to_be_invoiced' => $toBeInvoiced, + 'percent_value' => $percentValue, + 'line_items' => json_encode($lineItems), + 'quotation_custom_field' => CRM_Core_BAO_CustomField::getCustomFieldID('Quotation', 'Opportunity_Details', TRUE), + 'case_custom_field' => CRM_Core_BAO_CustomField::getCustomFieldID('Case_Opportunity', 'Opportunity_Details', TRUE), + ]); + } + + /** + * Determines if the hook will run. + * + * This hook is only valid for the Case form. + * + * The civicase client id parameter must be defined. + * + * @param CRM_Core_Form $form + * Form class. + * @param string $formName + * Form Name. + * @param int|null $salesOrderId + * Sales Order ID. + */ + public function shouldRun(CRM_Core_Form $form, string $formName, ?int $salesOrderId) { + return $formName === 'CRM_Contribute_Form_Contribution' + && $form->_action == CRM_Core_Action::ADD + && !empty($salesOrderId); + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php new file mode 100644 index 000000000..6150bc8e4 --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php @@ -0,0 +1,72 @@ +shouldRun($form, $formName)) { + return; + } + + $contributionId = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE); + $salesOrder = Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Quotation', 'IS NOT EMPTY') + ->addWhere('id', 'IN', explode(',', $contributionId)) + ->addChain('salesOrder', CaseSalesOrder::get() + ->addWhere('id', '=', '$Opportunity_Details.Quotation') + ) + ->execute() + ->getArrayCopy(); + + if (!empty($salesOrder)) { + $form->add('checkbox', 'attach_quote', ts('Attach Quotation')); + $form->setDefaults(array_merge($form->_defaultValues, ['attach_quote' => TRUE])); + } + + CRM_Core_Region::instance('page-body')->add([ + 'template' => "CRM/Civicase/Form/Contribute/AttachQuotation.tpl", + ]); + } + + /** + * Determines if the hook will run. + * + * @param CRM_Core_Form $form + * Form Class object. + * @param string $formName + * Form Name. + * + * @return bool + * TRUE if the hook should run, FALSE otherwise. + */ + private function shouldRun($form, $formName) { + if (!in_array($formName, [ + 'CRM_Contribute_Form_Task_Invoice', + 'CRM_Invoicehelper_Contribute_Form_Task_Invoice', + ])) { + return FALSE; + } + + $contributionId = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE); + if (!$contributionId) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php b/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php new file mode 100644 index 000000000..8a799804b --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php @@ -0,0 +1,49 @@ +shouldRun($form, $formName)) { + return; + } + + CRM_Core_Resources::singleton()->addScript( + "CRM.$(function($) { + $(\"a[target='crm-popup']\").on('crmPopupFormSuccess', function (e) { + CRM.refreshParent(e); + }); + }); + "); + } + + /** + * Determines if the hook will run. + * + * @param CRM_Core_Form $form + * Form Class object. + * @param string $formName + * Form Name. + * + * @return bool + * TRUE if the hook should run, FALSE otherwise. + */ + private function shouldRun($form, $formName) { + if ($formName !== 'CRM_Contribute_Form_Contribution' || $form->getAction() !== CRM_Core_Action::UPDATE || !isset($_GET['snippet'])) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/CRM/Civicase/Hook/CaseCategoryFormHookBase.php b/CRM/Civicase/Hook/CaseCategoryFormHookBase.php index 7294bcb20..155558345 100644 --- a/CRM/Civicase/Hook/CaseCategoryFormHookBase.php +++ b/CRM/Civicase/Hook/CaseCategoryFormHookBase.php @@ -14,6 +14,11 @@ class CRM_Civicase_Hook_CaseCategoryFormHookBase { */ const INSTANCE_TYPE_FIELD_NAME = 'case_category_instance_type'; + /** + * Instance field name. + */ + const FEATURES_FIELD_NAME = 'case_category_features'; + /** * Determines if the given form is a case type categories form. * diff --git a/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php b/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php new file mode 100644 index 000000000..01cdbf58c --- /dev/null +++ b/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php @@ -0,0 +1,45 @@ + &$value) { + if ($value['attributes']['name'] === $menuBefore) { + $weight = $desiredWeight = (int) $value['attributes']['weight']; + $moveDown = TRUE; + } + + if ($moveDown) { + $value['attributes']['weight'] = ++$weight; + } + } + + return $desiredWeight; + } + +} diff --git a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php index f71494176..00f914acc 100644 --- a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php +++ b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php @@ -1,12 +1,13 @@ caseCategorySetting = new CaseCategorySetting(); $this->rewriteCaseUrls($menu); $this->addCaseWebformUrl($menu); + $this->addCiviCaseInstanceMenu($menu); } /** @@ -116,4 +118,51 @@ private function menuWalk(array &$menu, callable $callback) { } } + /** + * Adds the civicase instance menu to the Adminsiter Civicase Menu. + * + * @param array $menu + * Tree of menu items, per hook_civicrm_navigationMenu. + */ + private function addCiviCaseInstanceMenu(array &$menu) { + $groupId = $this->getCaseTypeCategoryGroupId(); + if (empty($groupId)) { + return; + } + + // Find the Civicase menu. + $caseID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', 'CiviCase', 'id', 'name'); + $administerID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', 'Administer', 'id', 'name'); + $civicaseSettings = &$menu[$administerID]['child'][$caseID]; + + $desiredWeight = $this->moveMenuDown($civicaseSettings['child'], 'Case Types'); + + $menu[$administerID]['child'][$caseID]['child'][] = [ + 'attributes' => [ + 'label' => ts('CiviCase Instances'), + 'name' => 'CiviCase Instances', + 'url' => "civicrm/admin/options?gid=$groupId&reset=1", + 'permission' => 'access all cases and activities', + 'operator' => 'OR', + 'separator' => 1, + 'parentID' => $caseID, + 'active' => 1, + 'weight' => $desiredWeight, + ], + ]; + } + + /** + * Returnd the ID of the case type category option group. + */ + private function getCaseTypeCategoryGroupId() { + $optionGroups = OptionGroup::get() + ->addSelect('id') + ->addWhere('name', '=', 'case_type_categories') + ->setLimit(25) + ->execute(); + + return $optionGroups[0]["id"] ?? NULL; + } + } diff --git a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php new file mode 100644 index 000000000..a3c4ec6e8 --- /dev/null +++ b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php @@ -0,0 +1,76 @@ +addFeaturesMenu($menu); + } + + /** + * Adds enabled features menu to menu. + * + * @param array $menu + * Tree of menu items, per hook_civicrm_navigationMenu. + */ + private function addFeaturesMenu(array &$menu) { + try { + $caseTypeCategoryFeatures = new CaseTypeCategoryFeatures(); + $caseInstancesGroup = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures(self::FEATURES_WITH_MENU); + + foreach ($caseInstancesGroup as $caseInstances) { + $separator = 0; + $caseInstanceMenu = &$menu[$caseInstances['navigation_id']]; + $caseInstanceName = $caseInstances['name']; + $caseInstanceName = ($caseInstanceName === 'Prospecting') ? 'prospect' : $caseInstanceName; + $desiredWeight = $this->moveMenuDown($caseInstanceMenu['child'], "manage_{$caseInstanceName}_workflows"); + + foreach ($caseInstances['items'] as $caseInstance) { + $caseInstanceMenu['child'][] = [ + 'attributes' => [ + 'label' => ts('Manage ' . $caseInstance['feature_id:label']), + 'name' => 'Manage ' . $caseInstance['feature_id:label'], + 'url' => "civicrm/case-features/a?case_type_category={$caseInstance['category_id']}#/{$caseInstance['feature_id:name']}", + 'permission' => $caseInstanceMenu['attributes']['permission'], + 'operator' => 'OR', + 'parentID' => $caseInstanceMenu['attributes']['navID'], + 'active' => 1, + 'separator' => $separator++, + 'weight' => $desiredWeight, + ], + ]; + } + } + } + catch (\Throwable $th) { + \Civi::log()->error(E::ts("Error adding case instance features menu"), [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ]); + } + } + +} diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php new file mode 100644 index 000000000..a729856e9 --- /dev/null +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -0,0 +1,134 @@ +shouldRun($op, $objectName, $objectRef)) { + return; + } + + $financialTrnxId = $objectRef->financial_trxn_id; + if (empty($financialTrnxId)) { + return; + } + + $contributionId = $this->getContributionId($financialTrnxId); + if (empty($contributionId)) { + return; + } + + $contribution = Contribution::get() + ->addSelect('Opportunity_Details.Case_Opportunity', 'Opportunity_Details.Quotation') + ->addWhere('id', '=', $contributionId) + ->execute() + ->first(); + + $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation'] ?: NULL); + $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity'] ?: NULL); + } + + /** + * Updates CaseSalesOrder financial statuses. + * + * @param int $salesOrderID + * CaseSalesOrder ID. + */ + private function updateQuotationFinancialStatuses(?int $salesOrderID): void { + if (empty($salesOrderID)) { + return; + } + + $transaction = CRM_Core_Transaction::create(); + + try { + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderID); + $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); + $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderID) + ->addValue('invoicing_status_id', $invoicingStatusID) + ->addValue('payment_status_id', $paymentStatusID) + ->execute(); + + $transaction->commit(); + } + catch (\Throwable $th) { + $transaction->rollback(); + CRM_Core_Error::statusBounce(ts('Error updating sales order statues')); + } + } + + /** + * Updates Case financial statuses. + * + * @param int? $caseId + * CaseSalesOrder ID. + */ + private function updateCaseOpportunityFinancialDetails(?int $caseId) { + if (empty($caseId)) { + return; + } + + try { + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); + } + catch (\Throwable $th) { + CRM_Core_Error::statusBounce(ts('Error updating opportunity details')); + } + } + + /** + * Gets Contribution ID by Financial Transaction ID. + */ + private function getContributionId($financialTrxnId) { + $entityFinancialTrxn = civicrm_api3('EntityFinancialTrxn', 'get', [ + 'sequential' => 1, + 'entity_table' => 'civicrm_contribution', + 'financial_trxn_id' => $financialTrxnId, + ]); + + if (empty($entityFinancialTrxn['values'][0])) { + return NULL; + } + + return $entityFinancialTrxn['values'][0]['entity_id']; + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * @param string $objectRef + * The hook object reference. + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName, $objectRef) { + return $objectName == 'EntityFinancialTrxn' && $op == 'create' && property_exists($objectRef, 'financial_trxn_id'); + } + +} diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php new file mode 100644 index 000000000..e93bf6078 --- /dev/null +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -0,0 +1,100 @@ +shouldRun($op, $objectName)) { + return; + } + + $salesOrderId = CRM_Utils_Request::retrieve('sales_order', 'Integer'); + if (empty($salesOrderId)) { + $salesOrderId = $this->getQuotationID($objectId)['Opportunity_Details.Quotation']; + } + + if (empty($salesOrderId)) { + return; + } + + $salesOrder = CaseSalesOrder::get() + ->addSelect('status_id', 'case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $salesOrderStatusId = CRM_Utils_Request::retrieve('sales_order_status_id', 'Integer'); + if (empty($salesOrderStatusId)) { + $salesOrder = $salesOrder['status_id']; + } + + $transaction = CRM_Core_Transaction::create(); + try { + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); + $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('status_id', $salesOrderStatusId) + ->addValue('invoicing_status_id', $invoicingStatusID) + ->addValue('payment_status_id', $paymentStatusID) + ->execute(); + + $caseId = $salesOrder['case_id']; + if (empty($caseId)) { + return; + } + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); + } + catch (\Throwable $th) { + $transaction->rollback(); + CRM_Core_Error::statusBounce(ts('Error creating sales order contribution')); + } + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName) { + return strtolower($objectName) == 'contribution' && $op == 'create'; + } + + /** + * Gets quotation ID by contribution ID. + */ + private function getQuotationId($id) { + return Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('id', '=', $id) + ->execute() + ->first(); + } + +} diff --git a/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php new file mode 100644 index 000000000..acee44045 --- /dev/null +++ b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php @@ -0,0 +1,60 @@ +shouldRun($form, $formName)) { + return; + } + + $caseCategoryValues = $form->getVar('_submitValues'); + $caseCategory = $caseCategoryValues['value']; + + $this->saveCaseCategoryFeature($caseCategory, $caseCategoryValues); + } + + /** + * Saves the case category instance values. + * + * @param int $categoryId + * Case category id. + * @param array $submittedValues + * The key-value pair of submitted values. + */ + private function saveCaseCategoryFeature($categoryId, array $submittedValues) { + // Delete old features link. + CaseCategoryFeatures::delete() + ->addWhere('category_id', '=', $categoryId) + ->execute(); + + // Create new features link. + $caseCategoryFeatures = new CRM_Civicase_Service_CaseTypeCategoryFeatures(); + foreach ($caseCategoryFeatures->getFeatures() as $feature) { + if (!empty($submittedValues['case_category_feature_' . $feature['value']])) { + CaseCategoryFeatures::create() + ->addValue('category_id', $categoryId) + ->addValue('feature_id', $feature['value']) + ->execute(); + } + } + + } + +} diff --git a/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php new file mode 100644 index 000000000..781375f28 --- /dev/null +++ b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php @@ -0,0 +1,50 @@ +shouldRun($formName)) { + return; + } + + $this->saveQuotationsNoteField($form); + } + + /** + * Checks if this shook should run. + * + * @param string $formName + * Form Name. + * + * @return bool + * True if the hook should run. + */ + public function shouldRun($formName) { + return $formName == CRM_Admin_Form_Preferences_Contribute::class; + } + + /** + * Saves the Quotations Note Form field. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + public function saveQuotationsNoteField(CRM_Core_Form &$form) { + $values = $form->getVar('_submitValues'); + if (!empty($values['quotations_notes'])) { + Civi::settings()->set('quotations_notes', $values['quotations_notes']); + } + } + +} diff --git a/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php new file mode 100644 index 000000000..9dacd7a73 --- /dev/null +++ b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php @@ -0,0 +1,85 @@ +shouldRun($op, $objectName)) { + return; + } + + $salesOrderId = $this->getQuotationId($objectId); + if (empty($salesOrderId)) { + return; + } + + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator( + $salesOrderId, + (int) $objectId + ); + $invoicingStatusId = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $paymentStatusId = $caseSaleOrderContributionService->calculatePaymentStatus(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('invoicing_status_id', $invoicingStatusId) + ->addValue('payment_status_id', $paymentStatusId) + ->execute(); + + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator( + $caseSalesOrder['case_id'], + (int) $objectId + ); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName) { + return strtolower($objectName) == 'contribution' && $op == 'delete'; + } + + /** + * Gets quotation ID by contribution ID. + */ + private function getQuotationId($id) { + return Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('id', '=', $id) + ->execute() + ->first()['Opportunity_Details.Quotation']; + } + +} diff --git a/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php b/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php index 2f87fd3de..6e53bcfc6 100644 --- a/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php +++ b/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php @@ -8,6 +8,13 @@ */ class CRM_Civicase_Hook_Tabset_CaseCategoryTabAdd { + /** + * Case tab last weight. + * + * @var int + */ + public $caseTabWeight; + /** * Determines what happens if the hook is handled. * @@ -26,6 +33,8 @@ public function run($tabsetName, array &$tabs, array $context, &$useAng) { } $this->addCaseCategoryContactTabs($tabs, $context['contact_id'], $useAng); + $caseSalesOrdertab = new CRM_Civicase_Hook_Tabset_CaseSalesOrderTabAdd(); + $caseSalesOrdertab->addCaseSalesOrderTab($tabs, $context['contact_id'], $this->caseTabWeight++); } /** @@ -49,7 +58,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) } $permissionService = new CaseCategoryPermission(); - $caseTabWeight = $this->getCaseTabWeight($tabs); + $this->caseTabWeight = $this->getCaseTabWeight($tabs); foreach ($result['values'] as $caseCategory) { $caseCategoryPermissions = $permissionService->get($caseCategory['name']); $permissionsToCheck = $this->getBasicCaseCategoryPermissions($caseCategoryPermissions); @@ -57,7 +66,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) continue; } - $caseTabWeight++; + $this->caseTabWeight++; $useAng = TRUE; $icon = !empty($caseCategory['icon']) ? "crm-i {$caseCategory['icon']}" : ''; $tabs[] = [ @@ -67,7 +76,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) 'case_type_category' => $caseCategory['value'], ]), 'title' => ucfirst(strtolower($caseCategory['label'])), - 'weight' => $caseTabWeight, + 'weight' => $this->caseTabWeight, 'count' => CaseCategoryHelper::getCaseCount($caseCategory['name'], $contactID), 'class' => 'livePage', 'icon' => $icon, diff --git a/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php new file mode 100644 index 000000000..0d66a4c6f --- /dev/null +++ b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php @@ -0,0 +1,55 @@ +retrieveCaseInstanceWithEnabledFeatures(['quotations']); + + if (empty($caseInstances)) { + return; + } + + $tabs[] = [ + 'id' => 'quotations', + 'url' => CRM_Utils_System::url("civicrm/case-features/quotations/contact-tab?cid=$contactID"), + 'title' => 'Quotations', + 'weight' => $weight, + 'count' => $this->getContactSalesOrderCount($contactID), + 'icon' => '', + ]; + } + + /** + * Returns the number of sales order owned by a contact. + * + * @param int $contactID + * Contact ID to retrieve count for. + */ + public function getContactSalesOrderCount(int $contactID) { + $result = CaseSalesOrder::get() + ->addSelect('COUNT(id) AS count') + ->addWhere('client_id', '=', $contactID) + ->execute() + ->jsonSerialize(); + + return $result[0]['count']; + } + +} diff --git a/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php b/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php index 09d51b8e7..e746edc25 100644 --- a/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php +++ b/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php @@ -34,6 +34,12 @@ public function run(array &$tokens) { * Available tokens. */ private function setCaseTokenCategory(array &$tokens) { + if (CIVICRM_UF === 'UnitTests') { + // For unit tests where AddCaseCustomFieldsTokenValues might not be called + // using an empty key breaks the code. + return $tokens['case_cf'] = []; + } + $tokens['case_cf'][''] = ''; } diff --git a/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php new file mode 100644 index 000000000..b6e4dc930 --- /dev/null +++ b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php @@ -0,0 +1,82 @@ +execute()->jsonSerialize(); + foreach ($fields as $field) { + $label = E::ts('Quotation ' . ucwords(str_replace("_", " ", $field['name']))); + $e->entity(self::TOKEN)->register($field['name'], $label); + } + } + + /** + * Evaluates Token values. + * + * @param \Civi\Token\Event\TokenValueEvent $e + * TokenValue Event. + */ + public static function evaluateSalesOrderTokens(TokenValueEvent $e) { + $context = $e->getTokenProcessor()->context; + + if (array_key_exists('schema', $context) && in_array('salesOrderId', $context['schema'])) { + foreach ($e->getRows() as $row) { + if (!empty($row->context['salesOrderId'])) { + $salesOrderId = $row->context['salesOrderId']; + + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + foreach ($caseSalesOrder as $key => $value) { + try { + $row->tokens(self::TOKEN, $key, $value ?? ''); + } + catch (\Throwable $th) { + \Civi::log()->error( + 'Error resolving token: ' . self::TOKEN . '.' . $key, + [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ] + ); + } + } + } + } + } + + } + +} diff --git a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php new file mode 100644 index 000000000..d6a99919c --- /dev/null +++ b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php @@ -0,0 +1,84 @@ +shouldRun($params, $context, $shouldAttachQuote)) { + return; + } + + $contributionId = $params['tokenContext']['contributionId'] ?? $params['tplParams']['id']; + $rendered = $this->getContributionQuotationInvoice($contributionId); + + $attachment = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $rendered['html'], $rendered['format']); + + if ($attachment) { + $params['attachments']['quotaition_invoice'] = $attachment; + } + } + + /** + * Renders the Invoice for the quotation linked to the contribution. + * + * @param int $contributionId + * The contribution ID. + */ + private function getContributionQuotationInvoice($contributionId) { + $salesOrder = Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Quotation', 'IS NOT EMPTY') + ->addWhere('id', '=', $contributionId) + ->addChain('salesOrder', CaseSalesOrder::get() + ->addWhere('id', '=', '$Opportunity_Details.Quotation') + ) + ->execute() + ->first()['salesOrder']; + + if (empty($salesOrder)) { + return; + } + + /** @var \CRM_Civicase_Service_CaseSalesOrderInvoice */ + $invoiceService = new \CRM_Civicase_Service_CaseSalesOrderInvoice(new CRM_Civicase_WorkflowMessage_SalesOrderInvoice()); + return $invoiceService->render($salesOrder[0]['id']); + } + + /** + * Determines if the hook will run. + * + * @param array $params + * Mail parameters. + * @param string $context + * Mail context. + * @param string $shouldAttachQuote + * If the Attach Quote is set. + * + * @return bool + * returns TRUE if hook should run, FALSE otherwise. + */ + private function shouldRun(array $params, $context, $shouldAttachQuote) { + $component = $params['tplParams']['component'] ?? ''; + if ($component !== 'contribute' || empty($shouldAttachQuote)) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/CRM/Civicase/Page/CaseAngular.php b/CRM/Civicase/Page/CaseAngular.php index 0ea8ad317..a871565f1 100644 --- a/CRM/Civicase/Page/CaseAngular.php +++ b/CRM/Civicase/Page/CaseAngular.php @@ -20,7 +20,7 @@ class CRM_Civicase_Page_CaseAngular extends \CRM_Core_Page { public function run() { $loader = Civi::service('angularjs.loader'); $loader->setPageName('civicrm/case/a'); - $loader->addModules(['crmApp', 'civicase']); + $loader->addModules(['crmApp', 'civicase', 'civicase-features']); \Civi::resources()->addSetting([ 'crmApp' => [ 'defaultRoute' => '/case/list', diff --git a/CRM/Civicase/Page/CaseFeaturesAngular.php b/CRM/Civicase/Page/CaseFeaturesAngular.php new file mode 100644 index 000000000..a6cad6eb1 --- /dev/null +++ b/CRM/Civicase/Page/CaseFeaturesAngular.php @@ -0,0 +1,42 @@ +setPageName('civicrm/case-features/a'); + $loader->addModules(['crmApp', 'civicase-features']); + \Civi::resources()->addSetting([ + 'crmApp' => [ + 'defaultRoute' => '/quotations', + ], + ]); + + return parent::run(); + } + + /** + * Get Template File Name. + * + * @inheritdoc + */ + public function getTemplateFileName() { + return 'Civi/Angular/Page/Main.tpl'; + } + +} diff --git a/CRM/Civicase/Page/CaseSalesOrder.php b/CRM/Civicase/Page/CaseSalesOrder.php new file mode 100644 index 000000000..e010417b2 --- /dev/null +++ b/CRM/Civicase/Page/CaseSalesOrder.php @@ -0,0 +1,28 @@ +addModules(['crmApp', 'civicase-features']); + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + $this->assign('sales_order_id', $salesOrderId); + + return parent::run(); + } + +} diff --git a/CRM/Civicase/Page/ContactCaseSalesOrderTab.php b/CRM/Civicase/Page/ContactCaseSalesOrderTab.php new file mode 100644 index 000000000..678b9e65f --- /dev/null +++ b/CRM/Civicase/Page/ContactCaseSalesOrderTab.php @@ -0,0 +1,28 @@ +addModules(['crmApp', 'civicase-features']); + $contactId = CRM_Utils_Request::retrieveValue('cid', 'Positive'); + $this->assign('contactId', $contactId); + + return parent::run(); + } + +} diff --git a/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php new file mode 100644 index 000000000..ce7752b0f --- /dev/null +++ b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php @@ -0,0 +1,94 @@ +paymentStatusOptionValues = $this->getOptionValues('case_sales_order_payment_status'); + $this->invoicingStatusOptionValues = $this->getOptionValues('case_sales_order_invoicing_status'); + } + + /** + * Gets option values by option group name. + * + * @param string $name + * Option group name. + * + * @return array + * Option values. + * + * @throws API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function getOptionValues($name) { + return OptionValue::get(FALSE) + ->addSelect('*') + ->addWhere('option_group_id:name', '=', $name) + ->execute() + ->getArrayCopy(); + } + + /** + * Gets status (option values' value) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getValueFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['value']; + } + + /** + * Gets status (option values' label) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getLabelFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['label']; + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php new file mode 100644 index 000000000..063447175 --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php @@ -0,0 +1,219 @@ +salesOrder = $this->getSalesOrder($salesOrderId); + $this->deletingContributionId = $deletingContributionId; + $this->contributions = $this->getContributions(); + $this->totalInvoicedAmount = $this->getTotalInvoicedAmount(); + $this->totalPaymentsAmount = $this->getTotalPaymentsAmount(); + + } + + /** + * Calculates total invoiced amount. + * + * @return float + * Total invoiced amount. + */ + public function calculateTotalInvoicedAmount(): float { + + return $this->getTotalInvoicedAmount(); + } + + /** + * Calculates total paid amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalPaidAmount(): float { + return $this->getTotalPaymentsAmount(); + } + + /** + * Gets SalesOrder Total amount after tax. + */ + public function getQuotedAmount(): float { + return $this->salesOrder['total_after_tax']; + } + + /** + * Calculates invoicing status. + * + * @return string + * Invoicing status option value's value + */ + public function calculateInvoicingStatus() { + if (empty($this->salesOrder) || empty($this->contributions)) { + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); + } + + $quotationTotalAmount = $this->salesOrder['total_after_tax']; + if ($this->totalInvoicedAmount < $quotationTotalAmount) { + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); + } + + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); + } + + /** + * Calculates payment status. + * + * @return string + * Payment status option value's value + */ + public function calculatePaymentStatus() { + if (empty($this->salesOrder) || empty($this->contributions) || !($this->totalPaymentsAmount > 0)) { + + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); + } + + $quotationTotalAmount = $this->salesOrder['total_after_tax']; + if ($this->totalPaymentsAmount < $quotationTotalAmount) { + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); + } + + if ($this->totalPaymentsAmount > $quotationTotalAmount) { + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + } + + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); + } + + /** + * Gets list of contributions from the sale order. + * + * @return array + * List of contributions. + * + * @throws API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + private function getContributions() { + if (empty($this->salesOrder) || empty($this->salesOrder['id'])) { + return []; + } + + $contributions = Contribution::get(FALSE) + ->addWhere('Opportunity_Details.Quotation', '=', $this->salesOrder['id']); + + if ($this->deletingContributionId !== NULL) { + $contributions->addWhere('id', '!=', $this->deletingContributionId); + } + + return $contributions->execute()->getArrayCopy(); + } + + /** + * Gets Sales Order by SaleOrder ID. + * + * @param string $saleOrderId + * Sales Order ID. + * + * @return array|null + * Sales Order array or null + * + * @throws API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + private function getSalesOrder($saleOrderId) { + return CaseSalesOrder::get(FALSE) + ->addWhere('id', '=', $saleOrderId) + ->execute() + ->first(); + } + + /** + * Gets total invoiced amount. + * + * @return float + * Total invoiced amount. + */ + private function getTotalInvoicedAmount(): float { + $totalInvoicedAmount = 0.0; + foreach ($this->contributions as $contribution) { + $totalInvoicedAmount += $contribution['total_amount']; + } + + return $totalInvoicedAmount; + } + + /** + * Gets total payments amount. + * + * @return float + * Total payment amount. + */ + private function getTotalPaymentsAmount(): float { + $totalPaymentsAmount = 0.0; + foreach ($this->contributions as $contribution) { + $payments = civicrm_api3('Payment', 'get', [ + 'sequential' => 1, + 'contribution_id' => $contribution['id'], + ])['values']; + + foreach ($payments as $payment) { + $totalPaymentsAmount += $payment['total_amount']; + } + } + + return $totalPaymentsAmount; + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderInvoice.php b/CRM/Civicase/Service/CaseSalesOrderInvoice.php new file mode 100644 index 000000000..426b2352a --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderInvoice.php @@ -0,0 +1,142 @@ +addWhere('id', '=', $id) + ->addChain('items', CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', '$id') + ->addSelect('*', 'product_id.name', 'financial_type_id.name') + ) + ->addChain('computedRates', CaseSalesOrder::computeTotal() + ->setLineItems('$items') + ) + ->addChain('client', Contact::get() + ->addWhere('id', '=', '$client_id'), 0 + ) + ->execute() + ->first(); + + if (!empty($caseSalesOrder['client_id'])) { + $caseSalesOrder['clientAddress'] = Address::get() + ->addSelect('*', 'country_id:label', 'state_province_id:label') + ->addWhere('contact_id', '=', $caseSalesOrder['client_id']) + ->execute() + ->first(); + $caseSalesOrder['clientAddress']['country'] = $caseSalesOrder['clientAddress']['country_id:label']; + $caseSalesOrder['clientAddress']['state'] = $caseSalesOrder['clientAddress']['state_province_id:label']; + } + + $caseSalesOrder['taxRates'] = $caseSalesOrder['computedRates'][0]['taxRates'] ?? []; + $caseSalesOrder['quotation_date'] = date('Y-m-d', strtotime($caseSalesOrder['quotation_date'])); + + $domain = CRM_Core_BAO_Domain::getDomain(); + $organisation = Contact::get() + ->addSelect('image_URL') + ->addWhere('id', '=', $domain->contact_id) + ->execute() + ->first(); + + $model = new CRM_Civicase_WorkflowMessage_SalesOrderInvoice(); + $terms = self::getTerms(); + $model->setDomainLogo($organisation['image_URL']); + $model->setSalesOrder($caseSalesOrder); + $model->setTerms($terms); + $model->setSalesOrderId($id); + $model->setDomainLocation(self::getDomainLocation()); + $model->setDomainName($domain->name ?? ''); + $rendered = $model->renderTemplate(); + + $rendered['format'] = $rendered['format'] ?? self::defaultInvoiceFormat(); + + return $rendered; + } + + /** + * Returns the Quotation invoice terms. + */ + private static function getTerms() { + $terms = NULL; + $invoicing = Setting::get() + ->addSelect('invoicing') + ->execute() + ->first(); + + if (!empty($invoicing['value'])) { + $terms = Civi::settings()->get('quotations_notes'); + } + + return $terms; + } + + /** + * Gets domain location. + * + * @return array + * An array of address lines. + */ + private static function getDomainLocation() { + $domain = CRM_Core_BAO_Domain::getDomain(); + $locParams = ['contact_id' => $domain->contact_id]; + $locationDefaults = CRM_Core_BAO_Location::getValues($locParams); + if (empty($locationDefaults['address'][1])) { + return []; + } + $stateProvinceId = $locationDefaults['address'][1]['state_province_id'] ?? NULL; + $stateProvinceAbbreviationDomain = !empty($stateProvinceId) ? CRM_Core_PseudoConstant::stateProvinceAbbreviation($stateProvinceId) : ''; + $countryId = $locationDefaults['address'][1]['country_id']; + $countryDomain = !empty($countryId) ? CRM_Core_PseudoConstant::country($countryId) : ''; + + return [ + 'street_address' => CRM_Utils_Array::value('street_address', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_1' => CRM_Utils_Array::value('supplemental_address_1', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_2' => CRM_Utils_Array::value('supplemental_address_2', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_3' => CRM_Utils_Array::value('supplemental_address_3', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'city' => CRM_Utils_Array::value('city', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'postal_code' => CRM_Utils_Array::value('postal_code', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'state' => $stateProvinceAbbreviationDomain, + 'country' => $countryDomain, + ]; + } + + /** + * Returns the default format to use for Invoice. + */ + private static function defaultInvoiceFormat() { + return [ + 'margin_top' => 10, + 'margin_left' => 65, + 'metric' => 'px', + ]; + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php new file mode 100644 index 000000000..63379b52d --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -0,0 +1,171 @@ +setSalesOrder(); + } + + /** + * Sets the sales order value. + */ + private function setSalesOrder(): void { + $this->salesOrder = CaseSalesOrder::get() + ->addSelect('*') + ->addWhere('id', '=', $this->salesOrderId) + ->addChain('items', CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', '$id') + ->addSelect('*', 'product_id.name', 'financial_type_id.name') + ) + ->execute() + ->first() ?? []; + } + + /** + * Generates Line Items for the sales order entity. + */ + public function generateLineItems() { + $lineItems = []; + + if (empty($this->salesOrder['items'])) { + return []; + } + + $lineItems = $this->getLineItemForSalesOrder(); + + if ($this->type === self::INVOICE_REMAIN) { + $lineItems = [...$lineItems, ...$this->getPreviousContributionLineItem()]; + } + + return $lineItems; + } + + /** + * Returns the line items for a sales order. + * + * @return array + * Array of values keyed by contribution line item fields. + */ + private function getLineItemForSalesOrder() { + $items = []; + foreach ($this->salesOrder['items'] as $item) { + $item['quantity'] = ($this->type === self::INVOICE_PERCENT) ? + ($this->percentValue / 100) * $item['quantity'] : + $item['quantity']; + $item['total'] = $item['quantity'] * floatval($item['unit_price']); + $item['tax'] = empty($item['tax_rate']) ? 0 : $this->percent($item['tax_rate'], $item['total']); + + $items[] = $this->lineItemToContributionLineItem($item); + + if ($item['discounted_percentage'] > 0) { + $item['item_description'] = "{$item['item_description']} Discount {$item['discounted_percentage']}%"; + $item['unit_price'] = $this->percent($item['discounted_percentage'], -$item['unit_price']); + $item['total'] = $item['quantity'] * floatval($item['unit_price']); + $item['tax'] = empty($item['tax_rate']) ? 0 : $this->percent($item['tax_rate'], $item['total']); + $items[] = $this->lineItemToContributionLineItem($item); + } + } + + return $items; + } + + /** + * Returns the line items for a sales order. + * + * This is from the previously created contributions. + * + * @return array + * Array of values keyed by contribution line item fields. + */ + private function getPreviousContributionLineItem() { + $previousItems = []; + + $contributions = Contribution::get() + ->addSelect('*',) + ->addWhere('Opportunity_Details.Quotation', '=', $this->salesOrderId) + ->addChain('items', LineItem::get() + ->addSelect('qty', 'unit_price', 'tax_amount', 'line_total', 'entity_table', 'label', 'financial_type_id') + ->addWhere('contribution_id', '=', '$id') + ) + ->execute(); + + foreach ($contributions as $contribution) { + $items = $contribution['items']; + + if (empty($items)) { + continue; + } + + foreach ($items as $item) { + unset($item['id']); + $item['qty'] = $item['qty']; + $item['unit_price'] = -1 * $item['unit_price']; + $item['tax_amount'] = -1 * $item['tax_amount']; + $item['line_total'] = $item['qty'] * floatval($item['unit_price']); + $previousItems[] = $item; + } + } + + return $previousItems; + } + + /** + * Converts a sales order line item to a contribution line item. + * + * @param array $item + * Sales Order line item. + * + * @return array + * Contribution line item + */ + private function lineItemToContributionLineItem(array $item) { + return [ + 'qty' => $item['quantity'], + 'tax_amount' => round($item['tax'], 2), + 'label' => $item['item_description'], + 'entity_table' => 'civicrm_contribution', + 'financial_type_id' => $item['financial_type_id'], + 'line_total' => round($item['total'], 2), + 'unit_price' => round($item['unit_price'], 2), + ]; + } + + /** + * Returns percentage% of value. + * + * E.g. 5% of 10. + * + * @param float $percentage + * Percentage to calculate. + * @param float $value + * The value to get percentage of. + * + * @return float + * Calculated Percentage in float + */ + public function percent(float $percentage, float $value) { + return (floatval($percentage) / 100) * floatval($value); + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php new file mode 100644 index 000000000..971d7da57 --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php @@ -0,0 +1,184 @@ +caseId = $caseId; + $this->deletingContributionId = $deletingContributionId; + $contributions = $this->getContributions($caseId); + $this->calculateOpportunityFinancialAmount($contributions); + } + + /** + * Calculates total invoiced amount. + * + * @return float + * Total invoiced amount. + */ + public function calculateTotalInvoicedAmount(): float { + return $this->totalInvoicedAmount; + } + + /** + * Calculates total paid amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalPaidAmount(): float { + return $this->totalPaidAmount; + } + + /** + * Calculates total quoted amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalQuotedAmount(): float { + return $this->totalQuotedAmount; + } + + /** + * Calculates opportunity invoicing status. + * + * @return string + * Invoicing status option value's value + */ + public function calculateInvoicingStatus() { + if (!($this->totalInvoicedAmount > 0)) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); + } + + if ($this->totalInvoicedAmount < $this->totalQuotedAmount) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); + } + + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); + } + + /** + * Calculates opportunity payment status. + * + * @return string + * Payment status option value's value + */ + public function calculatePaymentStatus() { + if (!($this->totalPaidAmount > 0)) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount < $this->totalQuotedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount > $this->totalQuotedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + + } + + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); + } + + /** + * Updates opportunity financial details. + */ + public function updateOpportunityFinancialDetails(): void { + CiviCase::update() + ->addValue('Case_Opportunity_Details.Total_Amount_Quoted', $this->calculateTotalQuotedAmount()) + ->addValue('Case_Opportunity_Details.Total_Amount_Invoiced', $this->calculateTotalInvoicedAmount()) + ->addValue('Case_Opportunity_Details.Invoicing_Status', $this->calculateInvoicingStatus()) + ->addValue('Case_Opportunity_Details.Total_Amounts_Paid', $this->calculateTotalPaidAmount()) + ->addValue('Case_Opportunity_Details.Payments_Status', $this->calculatePaymentStatus()) + ->addWhere('id', '=', $this->caseId) + ->execute(); + } + + /** + * Calculates opportunity financial amounts. + * + * @param array $contributions + * List of contributions that link to the opportunity. + */ + private function calculateOpportunityFinancialAmount($contributions) { + $totalQuotedAmount = 0; + $totalInvoicedAmount = 0; + $totalPaidAmount = 0; + foreach ($contributions as $contribution) { + $salesOrderId = $contribution['Opportunity_Details.Quotation']; + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + + $totalQuotedAmount += $caseSaleOrderContributionService->getQuotedAmount(); + $totalPaidAmount += $caseSaleOrderContributionService->calculateTotalPaidAmount(); + $totalInvoicedAmount += $caseSaleOrderContributionService->calculateTotalInvoicedAmount(); + } + + $this->totalQuotedAmount = $totalQuotedAmount; + $this->totalPaidAmount = $totalPaidAmount; + $this->totalInvoicedAmount = $totalInvoicedAmount; + } + + /** + * Gets Contributions by case Id. + * + * @param int $caseId + * List of contributions that link to the opportunity. + */ + private function getContributions($caseId) { + $contributions = Contribution::get(FALSE) + ->addSelect('*', 'Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Case_Opportunity', '=', $caseId); + + if ($this->deletingContributionId !== NULL) { + $contributions->addWhere('id', '!=', $this->deletingContributionId); + } + + return $contributions->execute()->getArrayCopy(); + } + +} diff --git a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php new file mode 100644 index 000000000..94e557caf --- /dev/null +++ b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php @@ -0,0 +1,65 @@ +addSelect('id', 'label', 'value', 'name', 'option_group_id') + ->addWhere('option_group_id:name', '=', self::NAME) + ->execute(); + + return $optionValues; + } + + /** + * Retrieves case instance that has the defined features enabled. + * + * @param array $features + * The features to retrieve case instances for. + * + * @return array + * Array of Key\Pair value grouped by case instance id. + */ + public function retrieveCaseInstanceWithEnabledFeatures(array $features) { + $caseInstanceGroup = OptionGroup::get()->addWhere('name', '=', 'case_type_categories')->execute()[0] ?? NULL; + + if (empty($caseInstanceGroup)) { + return []; + } + + $result = CaseCategoryFeatures::get() + ->addSelect('*', 'option_value.label', 'option_value.name', 'feature_id:name', 'feature_id:label', 'navigation.id') + ->addJoin('OptionValue AS option_value', 'LEFT', + ['option_value.value', '=', 'category_id'] + ) + ->addJoin('Navigation AS navigation', 'LEFT', + ['navigation.name', '=', 'option_value.name'] + ) + ->addWhere('option_value.option_group_id', '=', $caseInstanceGroup['id']) + ->addWhere('feature_id:name', 'IN', $features) + ->execute(); + + $caseCategoriesGroup = array_reduce((array) $result, function (array $accumulator, array $element) { + $accumulator[$element['category_id']]['items'][] = $element; + $accumulator[$element['category_id']]['navigation_id'] = $element['navigation.id']; + $accumulator[$element['category_id']]['name'] = $element['option_value.name']; + + return $accumulator; + }, []); + + return $caseCategoriesGroup; + } + +} diff --git a/CRM/Civicase/Setup/Manage/AbstractManager.php b/CRM/Civicase/Setup/Manage/AbstractManager.php new file mode 100644 index 000000000..cdc918fb3 --- /dev/null +++ b/CRM/Civicase/Setup/Manage/AbstractManager.php @@ -0,0 +1,44 @@ +toggle(FALSE); + } + + /** + * Enables the entity. + */ + public function enable() { + $this->toggle(TRUE); + } + + /** + * Enables/Disables the entity based on the passed status. + * + * @params boolean $status + * True to enable the entity, False to disable the entity. + */ + abstract protected function toggle($status): void; + +} diff --git a/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php new file mode 100644 index 000000000..e35d20efb --- /dev/null +++ b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php @@ -0,0 +1,196 @@ +createSaleOrderStatus(); + $this->createSaleOrderInvoicingStatus(); + $this->createSaleOrderPaymentStatus(); + } + + /** + * Creates sale order status. + */ + private function createSaleOrderStatus() { + CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([ + 'name' => self::SALE_ORDER_STATUS_NAME, + 'title' => ts('Sales Order Status'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_STATUS_NAME, + 'name' => 'new', + 'label' => 'New', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_STATUS_NAME, + 'name' => 'sent_to_client', + 'label' => 'Sent to client', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_STATUS_NAME, + 'name' => 'accepted', + 'label' => 'Accepted', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_STATUS_NAME, + 'name' => 'deposit_paid', + 'label' => 'Deposit paid', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_STATUS_NAME, + 'name' => 'declined', + 'label' => 'Declined', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Creates sale order invoicing status. + */ + private function createSaleOrderInvoicingStatus() { + CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([ + 'name' => self::SALE_ORDER_INVOICING_STATUS_NANE, + 'title' => ts('Invoicing'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE, + 'name' => 'no_invoices', + 'label' => 'No Invoices', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE, + 'name' => 'partially_invoiced', + 'label' => 'Partially Invoiced', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE, + 'name' => 'fully_invoiced', + 'label' => 'Fully Invoiced', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Creates sale order payments status. + */ + private function createSaleOrderPaymentStatus() { + CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([ + 'name' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'title' => ts('Payments'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'name' => 'no_payments', + 'label' => 'No Payments', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'name' => 'no_payments', + 'label' => 'No Payments', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'name' => 'partially_paid', + 'label' => 'Partially Paid', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'name' => 'fully_paid', + 'label' => 'Fully Paid', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE, + 'name' => 'over_paid', + 'label' => 'Over Paid', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Removes the entity. + */ + public function remove(): void { + civicrm_api3('OptionGroup', 'get', [ + 'return' => ['id'], + 'name' => [ + 'IN' => [ + self::SALE_ORDER_STATUS_NAME, + self::SALE_ORDER_INVOICING_STATUS_NANE, + self::SALE_ORDER_PAYMENT_STATUS_NANE, + ], + ], + 'api.OptionGroup.delete' => ['id' => '$value.id'], + ]); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + civicrm_api3('OptionGroup', 'get', [ + 'sequential' => 1, + 'name' => [ + 'IN' => [ + self::SALE_ORDER_STATUS_NAME, + self::SALE_ORDER_INVOICING_STATUS_NANE, + self::SALE_ORDER_PAYMENT_STATUS_NANE, + ], + ], + 'api.OptionGroup.create' => ['id' => '$value.id', 'is_active' => $status], + ]); + } + +} diff --git a/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php new file mode 100644 index 000000000..ae6f0e9fc --- /dev/null +++ b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php @@ -0,0 +1,62 @@ + CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'title' => ts('Case Type Category Additional Features'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'name' => 'quotations', + 'label' => 'Quotations', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'name' => 'invoices', + 'label' => 'Invoices', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Removes the entity. + */ + public function remove(): void { + civicrm_api3('OptionGroup', 'get', [ + 'return' => ['id'], + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.delete' => ['id' => '$value.id'], + ]); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + civicrm_api3('OptionGroup', 'get', [ + 'sequential' => 1, + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.create' => ['id' => '$value.id', 'is_active' => $status], + ]); + } + +} diff --git a/CRM/Civicase/Setup/Manage/MembershipTypeCustomFieldManager.php b/CRM/Civicase/Setup/Manage/MembershipTypeCustomFieldManager.php new file mode 100644 index 000000000..710425708 --- /dev/null +++ b/CRM/Civicase/Setup/Manage/MembershipTypeCustomFieldManager.php @@ -0,0 +1,34 @@ + self::OPTION_GROUP_NAME, + "label" => "Membership Type", + "value" => "MembershipType", + "name" => "civicrm_membership_type", + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * {@inheritDoc} + */ + public function remove(): void {} + + /** + * {@inheritDoc} + */ + protected function toggle($status): void {} + +} diff --git a/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php b/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php new file mode 100644 index 000000000..bed8ba95f --- /dev/null +++ b/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php @@ -0,0 +1,84 @@ +addSelect('id') + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + + $templatePath = E::path('/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl'); + $templateBodyHtml = file_get_contents($templatePath); + + $params = [ + 'workflow_name' => SalesOrderInvoice::WORKFLOW, + 'msg_title' => 'Quotation Invoice', + 'msg_subject' => 'Quotation Invoice', + 'msg_html' => $templateBodyHtml, + 'is_reserved' => 0, + 'is_default' => 1, + ]; + + if (!empty($messageTemplate)) { + $params = array_merge(['id' => $messageTemplate['id']], $params); + } + + $optionValue = OptionValue::get(FALSE) + ->addWhere('option_group_id:name', '=', 'msg_tpl_workflow_case') + ->addWhere('name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + + if (empty($optionValue)) { + $optionValue = OptionValue::create(FALSE) + ->addValue('option_group_id.name', 'msg_tpl_workflow_case') + ->addValue('label', 'Quotation Invoice') + ->addValue('name', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + } + + $params['workflow_id'] = $optionValue['id']; + + MessageTemplate::save(FALSE)->addRecord($params)->execute(); + } + + /** + * {@inheritDoc} + */ + public function remove(): void { + MessageTemplate::delete(FALSE) + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute(); + + OptionValue::delete(FALSE) + ->addWhere('option_group_id:name', '=', 'msg_tpl_workflow_case') + ->addWhere('name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + MessageTemplate::update(FALSE) + ->addValue('is_active', $status) + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute(); + } + +} diff --git a/CRM/Civicase/Test/Fabricator/CaseType.php b/CRM/Civicase/Test/Fabricator/CaseType.php index 555539375..156047cff 100644 --- a/CRM/Civicase/Test/Fabricator/CaseType.php +++ b/CRM/Civicase/Test/Fabricator/CaseType.php @@ -44,6 +44,17 @@ class CRM_Civicase_Test_Fabricator_CaseType { */ public static function fabricate(array $params = []) { $params = array_merge(self::$defaultParams, $params); + + $result = civicrm_api3( + 'CaseType', + 'get', + ['name' => $params['name']] + ); + + if (!empty($result['values'])) { + $params['id'] = array_pop($result['values'])['id']; + } + $result = civicrm_api3( 'CaseType', 'create', diff --git a/CRM/Civicase/Test/Fabricator/Product.php b/CRM/Civicase/Test/Fabricator/Product.php new file mode 100644 index 000000000..7ced2b477 --- /dev/null +++ b/CRM/Civicase/Test/Fabricator/Product.php @@ -0,0 +1,38 @@ + 'test', + 'description' => 'test', + 'sku' => 'test', + 'price' => 20, + ]; + + /** + * Fabricate Case. + * + * @param array $params + * Parameters. + * + * @return mixed + * Api result. + */ + public static function fabricate(array $params = []) { + $params = array_merge(self::$defaultParams, $params); + $result = civicrm_api4('Product', 'create', [ + 'values' => $params, + ])->jsonSerialize(); + + return array_shift($result); + } + +} diff --git a/CRM/Civicase/Upgrader.php b/CRM/Civicase/Upgrader.php index 762e85d6b..d47880eeb 100644 --- a/CRM/Civicase/Upgrader.php +++ b/CRM/Civicase/Upgrader.php @@ -1,25 +1,29 @@ createManageCasesMenuItem(); + (new CaseTypeCategoryFeaturesManager())->create(); + (new CaseSalesOrderStatusManager())->create(); + (new QuotationTemplateManager())->create(); + (new MembershipTypeCustomFieldManager())->create(); } /** @@ -243,6 +251,10 @@ public function uninstall() { foreach ($steps as $step) { $step->apply(); } + + (new CaseTypeCategoryFeaturesManager())->remove(); + (new CaseSalesOrderStatusManager())->remove(); + (new QuotationTemplateManager())->remove(); } /** @@ -407,6 +419,10 @@ public function enable() { $workflowMenu = new AddManageWorkflowMenu(); $workflowMenu->apply(); + + (new CaseTypeCategoryFeaturesManager())->enable(); + (new CaseSalesOrderStatusManager())->enable(); + (new QuotationTemplateManager())->enable(); } /** @@ -416,6 +432,9 @@ public function disable() { $this->swapCaseMenuItems(); $this->toggleNav('Manage Cases', FALSE); + (new CaseTypeCategoryFeaturesManager())->disable(); + (new CaseSalesOrderStatusManager())->disable(); + (new QuotationTemplateManager())->disable(); } /** @@ -497,13 +516,13 @@ public function hasPendingRevisions() { * * @inheritdoc */ - public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { + public function enqueuePendingRevisions() { $currentRevisionNum = (int) $this->getCurrentRevision(); foreach ($this->getRevisions() as $revisionClass => $revisionNum) { if ($revisionNum <= $currentRevisionNum) { continue; } - $title = E::ts('Upgrade %1 to revision %2', [ + $title = ts('Upgrade %1 to revision %2', [ 1 => $this->extensionName, 2 => $revisionNum, ]); @@ -512,13 +531,8 @@ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { [(new $revisionClass())], $title ); - $queue->createItem($upgradeTask); - $setRevisionTask = new CRM_Queue_Task( - [get_class($this), '_queueAdapter'], - ['setCurrentRevision', $revisionNum], - $title - ); - $queue->createItem($setRevisionTask); + $this->queue->createItem($upgradeTask); + $this->appendTask($title, 'setCurrentRevision', $revisionNum); } } diff --git a/CRM/Civicase/Upgrader/Base.php b/CRM/Civicase/Upgrader/Base.php deleted file mode 100644 index 1c8340902..000000000 --- a/CRM/Civicase/Upgrader/Base.php +++ /dev/null @@ -1,396 +0,0 @@ -ctx = array_shift($args); - $instance->queue = $instance->ctx->queue; - $method = array_shift($args); - return call_user_func_array([$instance, $method], $args); - } - - /** - * CRM_Civicase_Upgrader_Base constructor. - * - * @param $extensionName - * @param $extensionDir - */ - public function __construct($extensionName, $extensionDir) { - $this->extensionName = $extensionName; - $this->extensionDir = $extensionDir; - } - - // ******** Task helpers ******** - - /** - * Run a CustomData file. - * - * @param string $relativePath - * the CustomData XML file path (relative to this extension's dir) - * @return bool - */ - public function executeCustomDataFile($relativePath) { - $xml_file = $this->extensionDir . '/' . $relativePath; - return $this->executeCustomDataFileByAbsPath($xml_file); - } - - /** - * Run a CustomData file - * - * @param string $xml_file - * the CustomData XML file path (absolute path) - * - * @return bool - */ - protected function executeCustomDataFileByAbsPath($xml_file) { - $import = new CRM_Utils_Migrate_Import(); - $import->run($xml_file); - return TRUE; - } - - /** - * Run a SQL file. - * - * @param string $relativePath - * the SQL file path (relative to this extension's dir) - * - * @return bool - */ - public function executeSqlFile($relativePath) { - CRM_Utils_File::sourceSQLFile( - CIVICRM_DSN, - $this->extensionDir . DIRECTORY_SEPARATOR . $relativePath - ); - return TRUE; - } - - /** - * Run the sql commands in the specified file. - * - * @param string $tplFile - * The SQL file path (relative to this extension's dir). - * Ex: "sql/mydata.mysql.tpl". - * - * @return bool - * @throws \CRM_Core_Exception - */ - public function executeSqlTemplate($tplFile) { - // Assign multilingual variable to Smarty. - $upgrade = new CRM_Upgrade_Form(); - - $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile; - $smarty = CRM_Core_Smarty::singleton(); - $smarty->assign('domainID', CRM_Core_Config::domainID()); - CRM_Utils_File::sourceSQLFile( - CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE - ); - return TRUE; - } - - /** - * Run one SQL query. - * - * This is just a wrapper for CRM_Core_DAO::executeSql, but it - * provides syntactic sugar for queueing several tasks that - * run different queries - * - * @return bool - */ - public function executeSql($query, $params = []) { - // FIXME verify that we raise an exception on error - CRM_Core_DAO::executeQuery($query, $params); - return TRUE; - } - - /** - * Syntactic sugar for enqueuing a task which calls a function in this class. - * - * The task is weighted so that it is processed - * as part of the currently-pending revision. - * - * After passing the $funcName, you can also pass parameters that will go to - * the function. Note that all params must be serializable. - */ - public function addTask($title) { - $args = func_get_args(); - $title = array_shift($args); - $task = new CRM_Queue_Task( - [get_class($this), '_queueAdapter'], - $args, - $title - ); - return $this->queue->createItem($task, ['weight' => -1]); - } - - // ******** Revision-tracking helpers ******** - - /** - * Determine if there are any pending revisions. - * - * @return bool - */ - public function hasPendingRevisions() { - $revisions = $this->getRevisions(); - $currentRevision = $this->getCurrentRevision(); - - if (empty($revisions)) { - return FALSE; - } - if (empty($currentRevision)) { - return TRUE; - } - - return ($currentRevision < max($revisions)); - } - - /** - * Add any pending revisions to the queue. - * - * @param CRM_Queue_Queue $queue - */ - public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { - $this->queue = $queue; - - $currentRevision = $this->getCurrentRevision(); - foreach ($this->getRevisions() as $revision) { - if ($revision > $currentRevision) { - $title = E::ts('Upgrade %1 to revision %2', [ - 1 => $this->extensionName, - 2 => $revision, - ]); - - // note: don't use addTask() because it sets weight=-1 - - $task = new CRM_Queue_Task( - [get_class($this), '_queueAdapter'], - ['upgrade_' . $revision], - $title - ); - $this->queue->createItem($task); - - $task = new CRM_Queue_Task( - [get_class($this), '_queueAdapter'], - ['setCurrentRevision', $revision], - $title - ); - $this->queue->createItem($task); - } - } - } - - /** - * Get a list of revisions. - * - * @return array - * revisionNumbers sorted numerically - */ - public function getRevisions() { - if (!is_array($this->revisions)) { - $this->revisions = []; - - $clazz = new ReflectionClass(get_class($this)); - $methods = $clazz->getMethods(); - foreach ($methods as $method) { - if (preg_match('/^upgrade_(.*)/', $method->name, $matches)) { - $this->revisions[] = $matches[1]; - } - } - sort($this->revisions, SORT_NUMERIC); - } - - return $this->revisions; - } - - public function getCurrentRevision() { - $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); - if (!$revision) { - $revision = $this->getCurrentRevisionDeprecated(); - } - return $revision; - } - - private function getCurrentRevisionDeprecated() { - $key = $this->extensionName . ':version'; - if ($revision = \Civi::settings()->get($key)) { - $this->revisionStorageIsDeprecated = TRUE; - } - return $revision; - } - - public function setCurrentRevision($revision) { - CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); - // clean up legacy schema version store (CRM-19252) - $this->deleteDeprecatedRevision(); - return TRUE; - } - - private function deleteDeprecatedRevision() { - if ($this->revisionStorageIsDeprecated) { - $setting = new CRM_Core_BAO_Setting(); - $setting->name = $this->extensionName . ':version'; - $setting->delete(); - CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n"); - } - } - - // ******** Hook delegates ******** - - /** - * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install - */ - public function onInstall() { - $files = glob($this->extensionDir . '/sql/*_install.sql'); - if (is_array($files)) { - foreach ($files as $file) { - CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); - } - } - $files = glob($this->extensionDir . '/sql/*_install.mysql.tpl'); - if (is_array($files)) { - foreach ($files as $file) { - $this->executeSqlTemplate($file); - } - } - $files = glob($this->extensionDir . '/xml/*_install.xml'); - if (is_array($files)) { - foreach ($files as $file) { - $this->executeCustomDataFileByAbsPath($file); - } - } - if (is_callable([$this, 'install'])) { - $this->install(); - } - } - - /** - * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall - */ - public function onPostInstall() { - $revisions = $this->getRevisions(); - if (!empty($revisions)) { - $this->setCurrentRevision(max($revisions)); - } - if (is_callable([$this, 'postInstall'])) { - $this->postInstall(); - } - } - - /** - * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall - */ - public function onUninstall() { - $files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl'); - if (is_array($files)) { - foreach ($files as $file) { - $this->executeSqlTemplate($file); - } - } - if (is_callable([$this, 'uninstall'])) { - $this->uninstall(); - } - $files = glob($this->extensionDir . '/sql/*_uninstall.sql'); - if (is_array($files)) { - foreach ($files as $file) { - CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); - } - } - } - - /** - * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable - */ - public function onEnable() { - // stub for possible future use - if (is_callable([$this, 'enable'])) { - $this->enable(); - } - } - - /** - * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable - */ - public function onDisable() { - // stub for possible future use - if (is_callable([$this, 'disable'])) { - $this->disable(); - } - } - - public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) { - switch ($op) { - case 'check': - return [$this->hasPendingRevisions()]; - - case 'enqueue': - return $this->enqueuePendingRevisions($queue); - - default: - } - } - -} diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php new file mode 100644 index 000000000..14cfd30cf --- /dev/null +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -0,0 +1,40 @@ +create(); + (new CaseTypeCategoryManager())->create(); + (new CaseSalesOrderStatusManager())->create(); + (new MembershipTypeCustomFieldManager())->create(); + } + catch (\Throwable $th) { + \Civi::log()->error('Error upgrading Civicase', [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ]); + } + + return TRUE; + } + +} diff --git a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php new file mode 100644 index 000000000..35eac01d0 --- /dev/null +++ b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php @@ -0,0 +1,75 @@ +lineItems)) { + $result[] = CaseSalesOrderBAO::computeTotal($this->lineItems); + } + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php new file mode 100644 index 000000000..d35fa3298 --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php @@ -0,0 +1,39 @@ +salesOrderID) { + return; + } + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); + $result['amount'] = $service->calculateTotalInvoicedAmount(); + } + + /** + * Sets Sales Order ID. + */ + public function setSalesOrderId(string $salesOrderId) { + $this->salesOrderID = $salesOrderId; + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php new file mode 100644 index 000000000..7b022ae53 --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php @@ -0,0 +1,38 @@ +salesOrderID) { + return; + } + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); + $result['amount'] = $service->calculateTotalPaidAmount(); + } + + /** + * Sets Sales Order ID. + */ + public function setSalesOrderId(string $salesOrderId) { + $this->salesOrderID = $salesOrderId; + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php new file mode 100644 index 000000000..ce7c3e37a --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -0,0 +1,218 @@ +createContribution(); + + return $result->exchangeArray($resultArray); + } + + /** + * {@inheritDoc} + */ + private function createContribution() { + $priceField = $this->getDefaultPriceSetFields(); + $createdContributionsCount = 0; + + foreach ($this->salesOrderIds as $id) { + $transaction = CRM_Core_Transaction::create(); + try { + $contribution = $this->createContributionWithLineItems($id, $priceField); + $this->linkCaseSalesOrderToContribution($id, $contribution['id']); + $this->updateCaseSalesOrderStatus($id); + $createdContributionsCount++; + } + catch (\Exception $e) { + $transaction->rollback(); + } + + $transaction->commit(); + } + + return ['created_contributions_count' => $createdContributionsCount]; + } + + /** + * Creates sales order contribution with associated line items. + * + * @param int $salesOrderId + * Sales Order ID. + * @param array $priceField + * Array of price fields. + */ + private function createContributionWithLineItems(int $salesOrderId, array $priceField): array { + $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue ?? 0); + $lineItems = $salesOrderContribution->generateLineItems(); + + $taxAmount = $lineTotal = 0; + $allLineItems = []; + foreach ($lineItems as $index => &$lineItem) { + $lineItem['price_field_id'] = $priceField[$index]['id']; + $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; + $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); + $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; + $taxAmount += (float) $lineItem['tax_amount'] ?? 0; + $lineTotal += (float) $lineItem['line_total'] ?? 0; + } + $totalAmount = $lineTotal + $taxAmount; + + if (round($totalAmount, 2) < 1) { + throw new \Exception("Contribution total amount must be greater than zero"); + } + + $params = [ + 'source' => "Quotation {$salesOrderId}", + 'line_item' => $allLineItems, + 'total_amount' => $totalAmount, + 'tax_amount' => $taxAmount, + 'financial_type_id' => $this->financialTypeId, + 'receive_date' => $this->date, + 'contact_id' => $salesOrderContribution->salesOrder['client_id'], + 'contribution_status_id' => $this->getPendingContributionStatusId(), + ]; + + return Contribution::create($params)->toArray(); + } + + /** + * Returns default contribution price set fields. + * + * @return array + * Array of price fields + */ + private function getDefaultPriceSetFields(): array { + $priceSet = PriceSet::get() + ->addWhere('name', '=', 'default_contribution_amount') + ->addWhere('is_quick_config', '=', 1) + ->execute() + ->first(); + + return PriceField::get() + ->addWhere('price_set_id', '=', $priceSet['id']) + ->addChain('price_field_value', PriceFieldValue::get() + ->addWhere('price_field_id', '=', '$id') + )->execute() + ->getArrayCopy(); + } + + /** + * Links sales order with contirbution. + * + * @param int $salesOrderId + * Sales Order Id. + * @param int $contributionId + * Contribution ID. + */ + private function linkCaseSalesOrderToContribution(int $salesOrderId, int $contributionId): void { + $salesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + Api4Contribution::update() + ->addValue('Opportunity_Details.Case_Opportunity', $salesOrder['case_id']) + ->addValue('Opportunity_Details.Quotation', $salesOrderId) + ->addWhere('id', '=', $contributionId) + ->execute(); + } + + /** + * Updates Sales Order status. + * + * @param int $salesOrderId + * Sales Order Id. + */ + private function updateCaseSalesOrderStatus(int $salesOrderId): void { + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('status_id', $this->statusId) + ->execute(); + } + + /** + * Returns ID for pending contribution status. + * + * @return int + * pending status ID + */ + private function getPendingContributionStatusId(): ?int { + $pendingStatus = OptionValue::get() + ->addSelect('value') + ->addWhere('option_group_id:name', '=', 'contribution_status') + ->addWhere('name', '=', 'pending') + ->execute() + ->first(); + + if (!empty($pendingStatus)) { + return $pendingStatus['value']; + } + + return NULL; + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php new file mode 100644 index 000000000..de525fec8 --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -0,0 +1,172 @@ +records as &$record) { + $record += $this->defaults; + $this->formatWriteValues($record); + $this->matchExisting($record); + if (empty($record['id'])) { + $this->fillDefaults($record); + $this->fillMandatoryFields($record); + } + } + $this->validateValues(); + + $resultArray = $this->writeRecord($this->records); + + $result->exchangeArray($resultArray); + } + + /** + * {@inheritDoc} + */ + protected function writeRecord($items) { + $transaction = CRM_Core_Transaction::create(); + + try { + $output = []; + foreach ($items as $salesOrder) { + $lineItems = $salesOrder['items']; + $this->validateLinItemProductPrice($lineItems); + $total = CaseSalesOrderBAO::computeTotal($lineItems); + $salesOrder['total_before_tax'] = $total['totalBeforeTax']; + $salesOrder['total_after_tax'] = $total['totalAfterTax']; + + $saleOrderId = $salesOrder['id'] ?? NULL; + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId); + $salesOrder['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $salesOrder['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); + + if (!is_null($saleOrderId)) { + $this->updateOpportunityDetails($saleOrderId); + } + + $salesOrders = $this->writeObjects([$salesOrder]); + $result = array_pop($salesOrders); + + $caseSalesOrderLineAPI = CaseSalesOrderLine::save(FALSE); + $this->removeStaleLineItems($salesOrder); + if (!empty($result) && !empty($lineItems)) { + array_walk($lineItems, function (&$lineItem) use ($result, $caseSalesOrderLineAPI) { + $lineItem['sales_order_id'] = $result['id']; + $lineItem['subtotal_amount'] = $lineItem['unit_price'] * $lineItem['quantity'] * ((100 - $lineItem['discounted_percentage']) / 100); + $caseSalesOrderLineAPI->addRecord($lineItem); + }); + + $result['items'] = $caseSalesOrderLineAPI->execute()->jsonSerialize(); + } + + $output[] = $result; + } + + return $output; + } + catch (\Exception $e) { + $transaction->rollback(); + + throw $e; + } + } + + /** + * Delete line items that have been detached. + * + * @param array $salesOrder + * Array of the salesorder to remove stale line items for. + */ + public function removeStaleLineItems(array $salesOrder) { + if (empty($salesOrder['id'])) { + return; + } + + $lineItemsInUse = array_column($salesOrder['items'], 'id'); + + if (empty($lineItemsInUse)) { + return; + } + + CaseSalesOrderLine::delete(FALSE) + ->addWhere('sales_order_id', '=', $salesOrder['id']) + ->addWhere('id', 'NOT IN', $lineItemsInUse) + ->execute(); + } + + /** + * Updates sales order's case opportunity details. + * + * @param int $salesOrderId + * Sales Order Id. + */ + private function updateOpportunityDetails($salesOrderId): void { + $caseSalesOrder = CaseSalesOrder::get(FALSE) + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + if (empty($caseSalesOrder) || empty($caseSalesOrder['case_id'])) { + return; + } + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + + /** + * Fill mandatory fields. + * + * @param array $params + * Single Sales Order Record. + */ + protected function fillMandatoryFields(&$params) { + $saleOrderId = $params['id'] ?? NULL; + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId); + $params['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $params['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); + } + + /** + * Ensures the product price doesnt exceed the max price. + * + * @param array $lineItems + * Sales Order line items. + */ + protected function validateLinItemProductPrice(array &$lineItems) { + array_walk($lineItems, function (&$lineItem) { + if (!empty($lineItem['product_id'])) { + $product = Product::get(FALSE) + ->addSelect('cost') + ->addWhere('id', '=', $lineItem['product_id']) + ->execute() + ->first(); + + if ($product && !empty($product['cost']) && $product['cost'] < $lineItem['subtotal_amount']) { + $lineItem['unit_price'] = $product['cost']; + $lineItem['quantity'] = 1; + $lineItem['subtotal_amount'] = CaseSalesOrderBAO::getSubTotal($lineItem); + } + } + }); + } + +} diff --git a/Civi/Api4/CaseCategoryFeatures.php b/Civi/Api4/CaseCategoryFeatures.php new file mode 100644 index 000000000..f8cf59b46 --- /dev/null +++ b/Civi/Api4/CaseCategoryFeatures.php @@ -0,0 +1,16 @@ +setCheckPermissions($checkPermissions); + } + + /** + * Compute the sum of the line items value. + * + * @param bool $checkPermissions + * Should permission be checked for the user. + * + * @return Civi\Api4\Action\CaseSalesOrder\ComputeTotalAction + * returns computed total action + */ + public static function computeTotal($checkPermissions = FALSE) { + return (new ComputeTotalAction(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * Computes the sum of amount paid. + * + * @param bool $checkPermissions + * Should permission be checked for the user. + * + * @return ComputeAmountPaidAction + * returns computed amount paid action + */ + public static function computeTotalAmountPaid(bool $checkPermissions = FALSE) { + return (new ComputeTotalAmountPaidAction(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * Computes the sum of amount invoiced. + * + * @param bool $checkPermissions + * Should permission be checked for the user. + * + * @return ComputeAmountInvoicedAction + * returns computed amount invoiced action + */ + public static function computeTotalAmountInvoiced(bool $checkPermissions = FALSE) { + return (new ComputeTotalAmountInvoicedAction(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * {@inheritDoc} + */ + public static function permissions() { + return [ + 'meta' => ['access CiviCRM'], + 'default' => ['access CiviCRM'], + ]; + } + +} diff --git a/Civi/Api4/CaseSalesOrderLine.php b/Civi/Api4/CaseSalesOrderLine.php new file mode 100644 index 000000000..806fc018b --- /dev/null +++ b/Civi/Api4/CaseSalesOrderLine.php @@ -0,0 +1,26 @@ + ['access CiviCRM'], + 'default' => ['access CiviCRM'], + ]; + } + +} diff --git a/Civi/Utils/CurrencyUtils.php b/Civi/Utils/CurrencyUtils.php new file mode 100644 index 000000000..f2f15b1b0 --- /dev/null +++ b/Civi/Utils/CurrencyUtils.php @@ -0,0 +1,43 @@ +fetch()) { + self::$currencies[] = [ + 'name' => $dao->name, + 'symbol' => $dao->symbol, + 'format' => CRM_Utils_Money::format(1234.56, $dao->name), + ]; + } + } + + return self::$currencies; + } + +} diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html new file mode 100644 index 000000000..eda87620a --- /dev/null +++ b/ang/afsearchContactQuotations.aff.html @@ -0,0 +1,25 @@ +
+
+
+ + + +
+
+ +
+
+
+ + +
+ +
+
+
+ +
diff --git a/ang/afsearchContactQuotations.aff.json b/ang/afsearchContactQuotations.aff.json new file mode 100644 index 000000000..3236e4480 --- /dev/null +++ b/ang/afsearchContactQuotations.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Contact Quotations", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/afsearchQuotationInvoices.aff.html b/ang/afsearchQuotationInvoices.aff.html new file mode 100644 index 000000000..44a6bc143 --- /dev/null +++ b/ang/afsearchQuotationInvoices.aff.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/afsearchQuotationInvoices.aff.json b/ang/afsearchQuotationInvoices.aff.json new file mode 100644 index 000000000..494c1f075 --- /dev/null +++ b/ang/afsearchQuotationInvoices.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Quotation Invoice", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html new file mode 100644 index 000000000..b6d80d2a5 --- /dev/null +++ b/ang/afsearchQuotations.aff.html @@ -0,0 +1,28 @@ +
+
+
+ + + + +
+
+ +
+
+ +
+ + +
+ +
+
+
+ +
+ diff --git a/ang/afsearchQuotations.aff.json b/ang/afsearchQuotations.aff.json new file mode 100644 index 000000000..4d37e2e0c --- /dev/null +++ b/ang/afsearchQuotations.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Quotations", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/civicase-features.ang.php b/ang/civicase-features.ang.php new file mode 100644 index 000000000..6a397ae7b --- /dev/null +++ b/ang/civicase-features.ang.php @@ -0,0 +1,94 @@ +retrieveCaseInstanceWithEnabledFeatures([$feature]); + $options['featureCaseTypes'][$feature] = array_keys($caseTypeCategories); + }, ['quotations', 'invoices']); +} + +/** + * Exposes case sales order statuses to Angular. + */ +function set_case_sales_order_status(&$options) { + $optionValues = OptionValue::get() + ->addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute(); + + $options['salesOrderStatus'] = $optionValues->getArrayCopy(); +} + +$requires = [ + 'api4', + 'crmUi', + 'crmUtil', + 'civicase', + 'ui.sortable', + 'dialogService', + 'civicase-base', + 'afsearchQuotations', + 'afsearchContactQuotations', + 'afsearchQuotationInvoices', +]; + +return [ + 'css' => [ + 'css/*.css', + ], + 'js' => getFeaturesJsFiles(), + 'settings' => $options, + 'requires' => $requires, + 'partials' => [ + 'ang/civicase-features', + ], +]; diff --git a/ang/civicase-features.js b/ang/civicase-features.js new file mode 100644 index 000000000..540167ef4 --- /dev/null +++ b/ang/civicase-features.js @@ -0,0 +1,3 @@ +(function (angular, $, _) { + angular.module('civicase-features', CRM.angRequires('civicase-features')); +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/app.routes.js b/ang/civicase-features/app.routes.js new file mode 100644 index 000000000..56645284a --- /dev/null +++ b/ang/civicase-features/app.routes.js @@ -0,0 +1,30 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.config(function ($routeProvider, UrlParametersProvider) { + $routeProvider.when('/quotations', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); + $routeProvider.when('/quotations/new', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); + $routeProvider.when('/invoices', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); + }); +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/invoices/directives/invoices-case-tab-content.html b/ang/civicase-features/invoices/directives/invoices-case-tab-content.html new file mode 100644 index 000000000..2b392f829 --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-case-tab-content.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.html b/ang/civicase-features/invoices/directives/invoices-list.directive.html new file mode 100644 index 000000000..39c38d16a --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.html @@ -0,0 +1,22 @@ +
+

{{ ts('Quotation Invoices') }}

+ + + add_circle + {{:: ts('Create Invoice') }} + + +
+
+ +
+
+
+ diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.js b/ang/civicase-features/invoices/directives/invoices-list.directive.js new file mode 100644 index 000000000..4ce2265de --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.js @@ -0,0 +1,31 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('invoicesList', function () { + return { + restrict: 'E', + controller: 'invoicesListController', + templateUrl: '~/civicase-features/invoices/directives/invoices-list.directive.html', + scope: {} + }; + }); + + module.controller('invoicesListController', invoicesListController); + + /** + * @param {object} $scope the controller scope + * @param {object} $location the location service + * @param {object} $window window object of the browser + */ + function invoicesListController ($scope, $location, $window) { + $scope.contributionURL = async () => { + let url = CRM.url('/contribute/add?reset=1&action=add&context=standalone'); + const caseId = $location.search().caseId; + if (caseId) { + url += `&caseId=${caseId}`; + } + + $window.location.href = url; + }; + } +})(angular, CRM._); diff --git a/ang/civicase-features/invoices/services/quotations-case-tab.service.js b/ang/civicase-features/invoices/services/quotations-case-tab.service.js new file mode 100644 index 000000000..1773d6030 --- /dev/null +++ b/ang/civicase-features/invoices/services/quotations-case-tab.service.js @@ -0,0 +1,17 @@ +(function (angular, $, _) { + var module = angular.module('civicase'); + + module.service('InvoicesCaseTab', InvoicesCaseTab); + + /** + * Invoices Case Tab service. + */ + function InvoicesCaseTab () { + /** + * @returns {string} Returns tab content HTMl template url. + */ + this.activeTabContentUrl = function () { + return '~/civicase-features/invoices/directives/invoices-case-tab-content.html'; + }; + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-case-tab-content.html b/ang/civicase-features/quotations/directives/quotations-case-tab-content.html new file mode 100644 index 000000000..ba39d3404 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-case-tab-content.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html new file mode 100644 index 000000000..8cdb5982e --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html @@ -0,0 +1,103 @@ +
+ +
+
+
+

+ {{:: ts('Create Bulk Contribution for %1 Quotations', {1: $ctrl.ids.length}) }} +

+
+
+
+
{{$ctrl.completedMessage}}
+
+ +
+
+ + +
+
+ + Amount is required +
+
+ +
+
+ + +
+
+ +
+ +
+ + Financial type is required +
+
+ +
+ +
+ + Date is required +
+
+ +
+ +
+ + Status is required +
+
+ +
+
+ +
+
{{:: ts('Creating Contributions..') }}
+
+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js new file mode 100644 index 000000000..3dd0a7dcf --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -0,0 +1,75 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationContributionBulk', function () { + return { + restrict: 'E', + controller: 'quotationContributionBulkController', + templateUrl: '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html', + scope: {} + }; + }); + + module.controller('quotationContributionBulkController', quotationContributionBulkController); + + /** + * @param {object} $q ng-promise object + * @param {object} $scope the controller scope + * @param {object} crmApi4 api V4 service + * @param {object} searchTaskBaseTrait searchkit trait + * @param {object} CaseUtils case utility service + * @param {object} SalesOrderStatus SalesOrderStatus service + */ + function quotationContributionBulkController ($q, $scope, crmApi4, searchTaskBaseTrait, CaseUtils, SalesOrderStatus) { + $scope.ts = CRM.ts('civicase'); + + const ctrl = angular.extend(this, $scope.model, searchTaskBaseTrait); + ctrl.stage = 'form'; + $scope.submitInProgress = false; + ctrl.data = { + toBeInvoiced: 'percent', + percentValue: null, + statusId: null, + financialTypeId: null, + date: $.datepicker.formatDate('yy-mm-dd', new Date()) + }; + ctrl.salesOrderStatus = SalesOrderStatus.getAll(); + this.progress = null; + const BATCH_SIZE = 50; + + (function () { + CaseUtils.getSalesOrderAndLineItems(ctrl.ids[0]).then((result) => { + ctrl.data.financialTypeId = result.items[0].financial_type_id ?? null; + ctrl.data.statusId = Number(result.status_id).toString(); + }); + })(); + + this.createBulkContribution = () => { + $q(async function (resolve, reject) { + ctrl.run = true; + ctrl.progress = 0; + let contributionCreated = 0; + let index = 0; + const chunkedIds = _.chunk(ctrl.ids, BATCH_SIZE); + for (const salesOrderIds of chunkedIds) { + try { + const result = await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, salesOrderIds }); + contributionCreated += result.created_contributions_count ?? 0; + } catch (error) { + console.log(error); + } finally { + index += BATCH_SIZE; + ctrl.progress = (index * 100) / ctrl.ids.length; + } + } + + ctrl.run = false; + ctrl.close(); + const contributionNotCreated = ctrl.ids.length - contributionCreated; + let message = `${contributionCreated} contributions have been generated`; + message += contributionNotCreated > 0 ? ` and no contributions were created for ${contributionNotCreated} quotes as there was no remaining amount to be invoiced` : ''; + CRM.alert(message, ts('Success'), 'success'); + }); + }; + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html new file mode 100644 index 000000000..ee144337f --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -0,0 +1,253 @@ +
+

{{ ts(isUpdate ? 'Edit Quotation':'Create Quotation') }}

+ +
+
+
+
+ +
+ + Client is required +
+
+
+ +
+ + Date is required +
+
+
+ +
+ + Description is required +
+
+
+ +
+ +
+
+
+ +
+ + Owner is required +
+
+
+ +
+ + Status is required +
+
+
+ +
+ + Currency is required +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
ProductItem Description Financial TypeUnit PriceQuantityDiscount %Tax %Subtotal
+ + + +
+ Description is required +
+ +
+ Financial Type is required +
+
+ {{ currencySymbol }} + +
+
+ Unit price is invalid +
+ +
+ Quantity is invalid +
+ +
+ Discount is invalid +
+ {{ roundTo(salesOrder.items[$index].tax_rate, 2) }} + {{ formatMoney(salesOrder.items[$index].subtotal_amount, salesOrder.currency) }}
+
+
+
+ +
+ +
+
+ +
+ +
+ + + + + + + +
Total{{ currencySymbol }} {{ formatMoney(salesOrder.total, salesOrder.currency) }}
Tax @ {{ i.rate }}%{{ currencySymbol }} {{ formatMoney(i.value, salesOrder.currency) }}
Grand Total{{ currencySymbol }} {{formatMoney(salesOrder.grandTotal, salesOrder.currency)}}
+
+
+ +
+ +
+ +
+
+
+
+ + +
+
+ + diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js new file mode 100644 index 000000000..b6721466e --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -0,0 +1,447 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsCreate', function () { + return { + restrict: 'E', + controller: 'quotationsCreateController', + templateUrl: '~/civicase-features/quotations/directives/quotations-create.directive.html', + scope: {} + }; + }); + + module.controller('quotationsCreateController', quotationsCreateController); + + /** + * @param {object} $scope the controller scope + * @param {object} $location the location service + * @param {object} $window window object of the browser + * @param {object} CurrencyCodes CurrencyCodes service + * @param {Function} civicaseCrmApi crm api service + * @param {object} Contact contact service + * @param {object} crmApi4 api V4 service + * @param {object} FeatureCaseTypes FeatureCaseTypes service + * @param {object} SalesOrderStatus SalesOrderStatus service + * @param {object} CaseUtils case utility service + * @param {object} crmUiHelp crm ui help service + */ + function quotationsCreateController ($scope, $location, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils, crmUiHelp) { + const defaultCurrency = 'GBP'; + const productsCache = new Map(); + const financialTypesCache = new Map(); + $scope.hs = crmUiHelp({ file: 'CRM/Civicase/SalesOrderCtrl' }); + + $scope.isUpdate = false; + $scope.formValid = true; + $scope.roundTo = roundTo; + $scope.formatMoney = formatMoney; + $scope.submitInProgress = false; + $scope.caseApiParam = caseApiParam; + $scope.saveQuotation = saveQuotation; + $scope.calculateSubtotal = calculateSubtotal; + $scope.currencyCodes = CurrencyCodes.getAll(); + $scope.handleClientChange = handleClientChange; + $scope.handleProductChange = handleProductChange; + $scope.membeshipTypesProductDiscountPercentage = 0; + $scope.handleCurrencyChange = handleCurrencyChange; + $scope.salesOrderStatus = SalesOrderStatus.getAll(); + $scope.defaultCaseId = $location.search().caseId || null; + $scope.handleFinancialTypeChange = handleFinancialTypeChange; + $scope.currencySymbol = CurrencyCodes.getSymbol(defaultCurrency); + + (function init () { + initializeSalesOrder(); + $scope.newSalesOrderItem = newSalesOrderItem; + CRM.wysiwyg.create('#sales-order-description'); + $scope.removeSalesOrderItem = removeSalesOrderItem; + + $scope.$on('totalChange', _.debounce(handleTotalChange, 250)); + }()); + + /** + * Initializess the sales order object + */ + function initializeSalesOrder () { + $scope.salesOrder = { + currency: defaultCurrency, + status_id: SalesOrderStatus.getValueByName('new'), + clientId: null, + quotation_date: $.datepicker.formatDate('yy-mm-dd', new Date()), + items: [{ + product_id: null, + item_description: null, + financial_type_id: null, + unit_price: null, + quantity: null, + discounted_percentage: null, + tax_rate: 0, + subtotal_amount: 0 + }], + total: 0, + grandTotal: 0, + case_id: $scope.defaultCaseId + }; + $scope.total = 0; + $scope.taxRates = []; + + setDefaultClientID(); + prefillSalesOrderForUpdate(); + } + + /** + * Pre-fill sales order when updating sales order. + */ + function prefillSalesOrderForUpdate () { + const salesOrderId = $location.search().id; + + if (!salesOrderId) { + $scope.salesOrder.owner_id = Contact.getCurrentContactID(); + return; + } + + CaseUtils.getSalesOrderAndLineItems(salesOrderId).then((result) => { + $scope.isUpdate = true; + $scope.salesOrder = result; + $scope.salesOrder.quotation_date = $.datepicker.formatDate('yy-mm-dd', new Date(result.quotation_date)); + $scope.salesOrder.status_id = (result.status_id).toString(); + CRM.wysiwyg.setVal('#sales-order-description', $scope.salesOrder.description); + $scope.$emit('totalChange'); + }); + } + + /** + * Sets client ID to case client. + */ + function setDefaultClientID () { + if (!$scope.defaultCaseId || $scope.isUpdate) { + return; + } + + crmApi4('CaseContact', 'get', { + select: ['contact_id'], + where: [['case_id', '=', $scope.defaultCaseId]] + }).then(function (caseContacts) { + if (Array.isArray(caseContacts) && caseContacts.length > 0) { + $scope.salesOrder.client_id = caseContacts[0].contact_id ?? null; + if ($scope.salesOrder.client_id) { + handleClientChange(); + } + } + }); + } + + /** + * Removes a sales order line item + * + * @param {number} index element index to be removed + */ + function removeSalesOrderItem (index) { + $scope.salesOrder.items.splice(index, 1); + $scope.$emit('totalChange'); + } + + /** + * Initializes empty sales order line item + */ + function newSalesOrderItem () { + $scope.salesOrder.items.push({ + product: null, + description: null, + financial_type_id: null, + unit_price: null, + quantity: null, + discounted_percentage: $scope.membeshipTypesProductDiscountPercentage, + tax_rate: 0, + subtotal_amount: 0 + }); + } + + /** + * Persists quotaiton and redirects on success + */ + function saveQuotation () { + if (!validateForm()) { + return; + } + + $scope.submitInProgress = true; + crmApi4('CaseSalesOrder', 'save', { records: [$scope.salesOrder] }) + .then(function (results) { + showSucessNotification(); + redirectToAppropraitePage(); + }, function (failure) { + $scope.submitInProgress = false; + CRM.alert('Unable to generate quotations', ts('Error'), 'error'); + }); + } + + /** + * Validates form before saving + * + * @returns {boolean} true if form is valid, otherwise false + */ + function validateForm () { + angular.forEach($scope.quotationsForm.$$controls, function (control) { + control.$setDirty(); + control.$validate(); + }); + + return $scope.quotationsForm.$valid; + } + + /** + * Updates description and unit price if user selects a product + * + * @param {*} index index of the sales order line item + */ + function handleProductChange (index) { + if (!$scope.salesOrder.items[index].product_id) { + $scope.salesOrder.items[index]['product_id.name'] = ''; + return; + } + const updateProductDependentFields = (productId) => { + $scope.salesOrder.items[index].item_description = productsCache.get(productId).description; + $scope.salesOrder.items[index].unit_price = parseFloat(productsCache.get(productId).price); + const financialTypeId = productsCache.get(productId).financial_type_id ?? null; + if (financialTypeId) { + $scope.salesOrder.items[index].financial_type_id = financialTypeId; + handleFinancialTypeChange(index); + } + calculateSubtotal(index); + }; + + const productId = $scope.salesOrder.items[index].product_id; + getProduct(productId).then(function (result) { + if (result) { + updateProductDependentFields(productId); + } + }); + } + + /** + * Applies any membership type product discounts to + * each sale order line item if the selected client has any memberships. + */ + function handleClientChange () { + const clientID = $scope.salesOrder.client_id; + crmApi4('Membership', 'get', { + select: ['membership_type_id.Product_Discounts.Product_Discount_Amount'], + where: [['contact_id', '=', clientID], ['status_id.is_current_member', '=', true]] + }).then(function (results) { + if (!results || results.length < 1) { + return; + } + let discountPercentage = 0; + results.forEach((membership) => { + discountPercentage += membership['membership_type_id.Product_Discounts.Product_Discount_Amount']; + }); + // make sure that the discount percentage cannot be more than 100% + if (discountPercentage > 100) { + discountPercentage = 100; + } + $scope.membeshipTypesProductDiscountPercentage = discountPercentage; + applySaleOrderItemPencentageDiscount(); + CRM.alert(ts('Automatic Members Discount Applied'), ts('Product Discount'), 'success'); + }); + } + + /** + * Applies Membership Type discounted percentage to + * each sale order item discounted percentage. + */ + function applySaleOrderItemPencentageDiscount () { + $scope.salesOrder.items.forEach((item) => { + item.discounted_percentage = item.discounted_percentage + $scope.membeshipTypesProductDiscountPercentage; + }); + } + + /** + * Update currency symbol if currecny field is upddated + */ + function handleCurrencyChange () { + $scope.currencySymbol = CurrencyCodes.getSymbol($scope.salesOrder.currency); + } + + /** + * Update tax filed and regenrate line item tax rates for line itme financial types + * + * @param {number} index index of the sales order line item + */ + function handleFinancialTypeChange (index) { + $scope.salesOrder.items[index].tax_rate = 0; + $scope.$emit('totalChange'); + + if ($scope.salesOrder.items[index]['financial_type_id.name']) { + $scope.salesOrder.items[index]['financial_type_id.name'] = ''; + } + + const updateFinancialTypeDependentFields = (financialTypeId) => { + $scope.salesOrder.items[index].tax_rate = financialTypesCache.get(financialTypeId).tax_rate; + $scope.$emit('totalChange'); + }; + + const financialTypeId = $scope.salesOrder.items[index].financial_type_id; + if (financialTypeId && financialTypesCache.has(financialTypeId)) { + updateFinancialTypeDependentFields(financialTypeId); + return; + } + + if (financialTypeId) { + civicaseCrmApi('EntityFinancialAccount', 'get', { + account_relationship: 'Sales Tax Account is', + entity_table: 'civicrm_financial_type', + entity_id: financialTypeId, + 'api.FinancialAccount.get': { id: '$value.financial_account_id' } + }) + .then(function (result) { + if (result.count > 0) { + financialTypesCache.set(financialTypeId, Object.values(result.values)[0]['api.FinancialAccount.get'].values[0]); + updateFinancialTypeDependentFields(financialTypeId); + } + }); + } + } + + /** + * Sums sales order line item without tax, and computes tax rates separately + * + * @param {number} index index of the sales order line item + */ + function calculateSubtotal (index) { + const item = $scope.salesOrder.items[index]; + if (!item) { + return; + } + + item.subtotal_amount = item.unit_price * item.quantity * ((100 - item.discounted_percentage) / 100) || 0; + $scope.$emit('totalChange'); + validateProductPrice(index); + } + + /** + * Ensures the product price doesnt exceed the max price + * + * @param {number} index index of the sales order line item + */ + async function validateProductPrice (index) { + const productId = $scope.salesOrder.items[index].product_id; + if (!productId) { + return; + } + + const product = await getProduct(productId); + const shouldCompareCost = product && product.cost > 0; + if (!shouldCompareCost) { + return; + } + + const cost = productsCache.get(productId).cost; + if ($scope.salesOrder.items[index].subtotal_amount > cost) { + $scope.salesOrder.items[index].quantity = 1; + $scope.salesOrder.items[index].unit_price = parseFloat(cost); + CRM.alert('The quotation line item(s) have been set to the maximum premium price', ts('info'), 'info'); + calculateSubtotal(index); + } + } + + /** + * Rounds floating ponumber n to specified number of places + * + * @param {*} n number to round + * @param {*} place decimal places to round to + * @returns {number} the rounded off number + */ + function roundTo (n, place) { + return +(Math.round(n + 'e+' + place) + 'e-' + place); + } + + /** + * Show Quotation success create notification + */ + function showSucessNotification () { + const msg = !$scope.isUpdate ? 'Your Quotation has been generated successfully.' : 'Details updated successfully'; + CRM.alert(msg, ts('Saved'), 'success'); + } + + /** + * Handles page rediection after successfully creating quotation. + * + * redirects to main quotation list page if no case is selected + * else redirects to the case view of the selected case. + */ + function redirectToAppropraitePage () { + if ($scope.isUpdate) { + $window.location.href = $window.document.referrer; + return; + } + + if (!$scope.salesOrder.case_id) { + $window.location.href = 'a#/quotations'; + } + + CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { + $window.location.href = `${link}&tab=Quotations`; + }); + } + + /** + * @returns {object} api parameters for Case.getlist + */ + function caseApiParam () { + const caseTypeCategoryId = FeatureCaseTypes.getCaseTypes('quotations'); + return { params: { 'case_id.case_type_id.case_type_category': { IN: caseTypeCategoryId } } }; + } + + /** + * Computes total and tax rates from API + */ + function handleTotalChange () { + crmApi4('CaseSalesOrder', 'computeTotal', { + lineItems: $scope.salesOrder.items + }).then(function (results) { + $scope.taxRates = results[0].taxRates; + $scope.salesOrder.total = results[0].totalBeforeTax; + $scope.salesOrder.grandTotal = results[0].totalAfterTax; + }, function (failure) { + // handle failure + }); + } + + /** + * Formats a number into the number format of the currently selected currency + * + * @param {number} value the number to be formatted + * @param {string } currency the selected currency + * @returns {number} the formatted number + */ + function formatMoney (value, currency) { + return CRM.formatMoney(value, true, CurrencyCodes.getFormat(currency)); + } + + /** + * Get product by ID + * + * @param {number} productId id of product to fetch + * + * @returns {Promise} product + */ + async function getProduct (productId) { + let product = null; + if (productsCache.has(productId)) { + return productsCache.get(productId); + } + + try { + const result = await civicaseCrmApi('Product', 'get', { id: productId }); + if (result.count > 0) { + product = result.values[productId]; + productsCache.set(productId, product); + } + } catch (error) { + return null; + } + + return product; + } + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-discount.directive.html b/ang/civicase-features/quotations/directives/quotations-discount.directive.html new file mode 100644 index 000000000..596b1fa6e --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-discount.directive.html @@ -0,0 +1,51 @@ +
+

Apply Discount

+ +
+ +
+
{{$ctrl.completedMessage}}
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
diff --git a/ang/civicase-features/quotations/directives/quotations-discount.directive.js b/ang/civicase-features/quotations/directives/quotations-discount.directive.js new file mode 100644 index 000000000..395631fb0 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-discount.directive.js @@ -0,0 +1,51 @@ +(function (angular, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsDiscount', function () { + return { + restrict: 'E', + controller: 'quotationsDiscountController', + templateUrl: '~/civicase-features/quotations/directives/quotations-discount.directive.html', + scope: {} + }; + }); + + module.controller('quotationsDiscountController', quotationsDiscountController); + + /** + * @param {object} $q ng-promise object + * @param {object} $scope the controller scope + * @param {object} crmApi4 api V4 service + * @param {object} searchTaskBaseTrait searchkit trait + * @param {object} CaseUtils case utility service + */ + function quotationsDiscountController ($q, $scope, crmApi4, searchTaskBaseTrait, CaseUtils) { + $scope.ts = CRM.ts('civicase'); + const ctrl = angular.extend(this, $scope.model, searchTaskBaseTrait); + ctrl.stage = 'form'; + $scope.submitInProgress = false; + + this.applyDiscount = () => { + $q(async function (resolve, reject) { + const updatedSalesOrder = {}; + + for (const salesOrderId of ctrl.ids) { + const result = await CaseUtils.getSalesOrderAndLineItems(salesOrderId); + const selectedProducts = ctrl.products.split(','); + result.items.forEach((lineItem, index) => { + // The line item is part of the product user desires to apply discount to + if (lineItem.product_id && selectedProducts.includes((lineItem.product_id).toString())) { + const newDiscount = result.items[index].discounted_percentage + ctrl.discount; + result.items[index].discounted_percentage = Math.min(100, newDiscount); + updatedSalesOrder[salesOrderId] = result; + } + }); + } + + await crmApi4('CaseSalesOrder', 'save', { records: Object.values(updatedSalesOrder) }); + ctrl.close(); + CRM.alert(`Discount applied to ${Object.values(updatedSalesOrder).length} Quotation(s) successfully`, ts('Success'), 'success'); + }); + }; + } +})(angular, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html new file mode 100644 index 000000000..e8327d827 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -0,0 +1,41 @@ +
+

{{ ts('Manage Quotations') }}

+ + + add_circle + {{:: ts('Create Quotation') }} + + +
+
+
+ + +
+ +
+
+
+ diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js new file mode 100644 index 000000000..1d414ff7a --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -0,0 +1,67 @@ +(function (angular, _, $) { + var module = angular.module('civicase-features'); + + module.directive('quotationsList', function () { + return { + restrict: 'E', + controller: 'quotationsListController', + templateUrl: '~/civicase-features/quotations/directives/quotations-list.directive.html', + scope: { + view: '@', + contactId: '@' + } + }; + }); + + module.controller('quotationsListController', quotationsListController); + + /** + * @param {object} $scope the controller scope + * @param {object} $location the location service + * @param {object} $window window object of the browser + */ + function quotationsListController ($scope, $location, $window) { + $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen; + + (function init () { + addEventToElementsWhenInDOMTree(); + }()); + + /** + * Redirects user to new quotation screen + */ + function redirectToQuotationCreationScreen () { + let url = CRM.url('/case-features/a#/quotations/new'); + const caseId = $location.search().caseId; + if (caseId) { + url += `?caseId=${caseId}`; + } + + $window.location.href = url; + } + + /** + * Add events to elements that are occasionally removed from DOM tree + */ + function addEventToElementsWhenInDOMTree () { + const observer = new window.MutationObserver(function (mutations) { + if ($('#ui-datepicker-div:visible a').length) { + // Prevents date picker from triggering route navigation. + $('#ui-datepicker-div:visible a').click((event) => { event.preventDefault(); }); + } + + if ($('.civicase__features-filters-clear').length) { + // Handle clear filter button. + $('.civicase__features-filters-clear').click(event => { + CRM.$('.civicase__features input, .civicase__features textarea').val('').change(); + }); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + } + } +})(angular, CRM._, CRM.$); diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html new file mode 100644 index 000000000..61ffba7a9 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -0,0 +1,177 @@ +
+

{{ ts('View Quotation') }}

+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Quotation Id{{salesOrder.id}}
Client{{salesOrder['client_id.display_name']}}
Date{{salesOrder.quotation_date}}
Description
Case/Opportunity
Case Id{{salesOrder.case_id}}
Case Type{{salesOrder['case_id.case_type_id:label']}}
Case Subject{{salesOrder['case_id.subject']}}
Owner{{salesOrder['owner_id.display_name']}}
Status{{salesOrder['status_id:label']}}
Currency{{salesOrder.currency}}
Invoicing{{salesOrder['invoicing_status_id:label']}}
Payments{{salesOrder['payment_status_id:label']}}
+
+
+ +
+
+

Items Overview

+
+
+ + + + + + + + + + + + + + + + + + + + + +
ProductItem Description Financial TypeUnit PriceQuantityDiscount %Tax %Subtotal
{{ item["product_id.name"] }}{{ item["item_description"] }}{{ item["financial_type_id.name"] }}{{ item["unit_price"] || 0 }}{{ item["quantity"] || 0 }}{{ item["discounted_percentage"] || 0 }}{{ item["tax_rate"] || 0 }}{{ item["subtotal_amount"] }}
+
+
+ +
+
+

Amount Summary

+
+
+ + + + + + + + + + + + + +
Total{{ currencySymbol }} {{ salesOrder.total_before_tax }}
Tax @ {{ i.rate }}%{{ currencySymbol }} {{ i.value }}
Grand Total{{ currencySymbol }} {{salesOrder.total_after_tax}}
+
+
+
+
+
+
+
+ + + + + + + + + +
Total Amount Invoiced{{ currencySymbol }} {{ salesOrder.totalAmountInvoiced.amount }}
Total Amount Paid{{ currencySymbol }} {{salesOrder.totalAmountPaid.amount}}
+
+
+ +
+
+

Notes

+
+
+
+

+ {{salesOrder.notes}} +

+
+ +
+
+
+ +
+ + diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.js b/ang/civicase-features/quotations/directives/quotations-view.directive.js new file mode 100644 index 000000000..a285eaf23 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.js @@ -0,0 +1,85 @@ +(function (angular, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsView', function () { + return { + restrict: 'E', + controller: 'quotationsViewController', + templateUrl: '~/civicase-features/quotations/directives/quotations-view.directive.html', + scope: { + salesOrderId: '=' + } + }; + }); + + module.controller('quotationsViewController', quotationsViewController); + + /** + * @param {object} crmApi4 api V4 service + * @param {object} $scope the controller scope + * @param {object} CaseUtils case utility service + * @param {object} CurrencyCodes CurrencyCodes service + */ + function quotationsViewController (crmApi4, $scope, CaseUtils, CurrencyCodes) { + $scope.taxRates = []; + $scope.currencySymbol = ''; + $scope.dashboardLink = '#'; + $scope.salesOrder = { + total_before_tax: 0, + total_after_tax: 0 + }; + $scope.hasCase = false; + $scope.getContactLink = getContactLink; + + (function init () { + if ($scope.salesOrderId) { + getSalesOrderAndLineItems(); + } + })(); + + /** + * Retrieves the sales order and its line items from API + */ + function getSalesOrderAndLineItems () { + crmApi4('CaseSalesOrder', 'get', { + select: ['*', 'case_sales_order_line.*', 'client_id.display_name', 'owner_id.display_name', 'case_id.case_type_id:label', 'case_id.subject', 'status_id:label', 'invoicing_status_id:label', 'payment_status_id:label'], + where: [['id', '=', $scope.salesOrderId]], + limit: 1, + chain: { + items: ['CaseSalesOrderLine', 'get', { + where: [['sales_order_id', '=', '$id']], + select: ['*', 'product_id.name', 'financial_type_id.name'] + }], + computedRates: ['CaseSalesOrder', 'computeTotal', { lineItems: '$items' }], + totalAmountInvoiced: ['CaseSalesOrder', 'computeTotalAmountInvoiced', { salesOrderId: '$id' }], + totalAmountPaid: ['CaseSalesOrder', 'computeTotalAmountPaid', { salesOrderId: '$id' }] + } + }).then(async function (caseSalesOrders) { + if (Array.isArray(caseSalesOrders) && caseSalesOrders.length > 0) { + $scope.salesOrder = caseSalesOrders.pop(); + $scope.salesOrder.taxRates = $scope.salesOrder.computedRates[0].taxRates; + $scope.currencySymbol = CurrencyCodes.getSymbol($scope.salesOrder.currency); + $scope.salesOrder.quotation_date = CRM.utils.formatDate($scope.salesOrder.quotation_date); + if (!$scope.salesOrder.case_id) { + return; + } + $scope.hasCase = true; + CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { + $scope.dashboardLink = `${link}&focus=1&tab=Quotations`; + }); + } + }); + } + + /** + * Returns link to the contact dashboard + * + * @param {number} id the contact ID + * + * @returns {string} dashboard link + */ + function getContactLink (id) { + return CRM.url(`/contact/view?reset=1&cid=${id}`); + } + } +})(angular, CRM._); diff --git a/ang/civicase-features/quotations/services/quotations-case-tab.service.js b/ang/civicase-features/quotations/services/quotations-case-tab.service.js new file mode 100644 index 000000000..2e22f7a6e --- /dev/null +++ b/ang/civicase-features/quotations/services/quotations-case-tab.service.js @@ -0,0 +1,17 @@ +(function (angular, $, _) { + var module = angular.module('civicase'); + + module.service('QuotationsCaseTab', QuotationsCaseTab); + + /** + * Quotations Case Tab service. + */ + function QuotationsCaseTab () { + /** + * @returns {string} Returns tab content HTMl template url. + */ + this.activeTabContentUrl = function () { + return '~/civicase-features/quotations/directives/quotations-case-tab-content.html'; + }; + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/shared/configs/add-features-tab.config.js b/ang/civicase-features/shared/configs/add-features-tab.config.js new file mode 100644 index 000000000..366fa75a4 --- /dev/null +++ b/ang/civicase-features/shared/configs/add-features-tab.config.js @@ -0,0 +1,58 @@ +(function (angular) { + const module = angular.module('civicase-features'); + + module.config(function ($windowProvider, tsProvider, CaseDetailsTabsProvider) { + const $window = $windowProvider.$get(); + const ts = tsProvider.$get(); + const featuresTab = [ + { + name: 'Quotations', + label: ts('Quotations'), + weight: 100 + }, { + name: 'Invoices', + label: ts('Invoices'), + weight: 110 + } + ]; + + featuresTab.forEach(feature => { + if (caseTypeCategoryHasFeatureEnabled(feature.name)) { + CaseDetailsTabsProvider.addTabs([feature]); + } + }); + + /** + * Returns the current case type category parameter. This is used instead of + * the $location service because the later is not available at configuration + * time. + * + * @returns {string|null} the name of the case type category, or null. + */ + function getCaseTypeCategory () { + const urlParamRegExp = /case_type_category=([^&]+)/i; + const currentSearch = decodeURIComponent($window.location.search); + const results = urlParamRegExp.exec(currentSearch); + + return results && results[1]; + } + + /** + * Returns true if the current case type category has quotations + * features enabled + * + * @param {string} feature THe name of the feature + * + * @returns {boolean} true if quotations is enabled, otherwise false + */ + function caseTypeCategoryHasFeatureEnabled (feature) { + const caseTypeCategory = parseInt(getCaseTypeCategory()); + const quotationCaseTypeCategories = CRM['civicase-features'].featureCaseTypes[feature.toLocaleLowerCase()] || []; + if (Array.isArray(quotationCaseTypeCategories) && caseTypeCategory) { + return quotationCaseTypeCategories.includes(caseTypeCategory); + } + + return false; + } + }); +})(angular); diff --git a/ang/civicase-features/shared/services/case-utils.service.js b/ang/civicase-features/shared/services/case-utils.service.js new file mode 100644 index 000000000..e8ed12dfc --- /dev/null +++ b/ang/civicase-features/shared/services/case-utils.service.js @@ -0,0 +1,55 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('CaseUtils', CaseUtils); + + /** + * CaseUtils Service + * + * @param {object} $q ng promise object + * @param {object} crmApi4 api V4 service + * @param {Function} civicaseCrmApi civicrm api service + */ + function CaseUtils ($q, crmApi4, civicaseCrmApi) { + /** + * Gets the link to a case dashboard + * + * @param {number} id ID of the case + * @returns {Promise} promise object + */ + this.getDashboardLink = function (id) { + return $q(function (resolve, reject) { + const params = { id, return: ['case_type_category', 'case_type_id'] }; + civicaseCrmApi('Case', 'getdetails', params) + .then(function (result) { + const categoryId = result.values[id].case_type_category; + const link = CRM.url(`/case/a/?case_type_category=${categoryId}#/case/list?cf={"case_type_category":"${categoryId}"}&caseId=${id}`); + + resolve(link); + }); + }); + }; + + /** + * Retrieves the sales order and its line items from API + * + * @param {number} salesOrderId ID of the sales order to retrieve + * @returns {Promise} promise object + */ + this.getSalesOrderAndLineItems = function (salesOrderId) { + return $q(function (resolve, reject) { + crmApi4('CaseSalesOrder', 'get', { + select: ['*', 'case_sales_order_line.*'], + where: [['id', '=', salesOrderId]], + limit: 1, + chain: { items: ['CaseSalesOrderLine', 'get', { where: [['sales_order_id', '=', '$id']], select: ['*', 'product_id.name', 'financial_type_id.name'] }] } + }).then(async function (caseSalesOrders) { + if (Array.isArray(caseSalesOrders) && caseSalesOrders.length > 0) { + const salesOrder = caseSalesOrders.pop(); + resolve(salesOrder); + } + }); + }); + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase-features/shared/services/currency-codes.service.js b/ang/civicase-features/shared/services/currency-codes.service.js new file mode 100644 index 000000000..fa467f2ab --- /dev/null +++ b/ang/civicase-features/shared/services/currency-codes.service.js @@ -0,0 +1,28 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('CurrencyCodes', CurrencyCodes); + + /** + * CurrencyCodes Service + */ + function CurrencyCodes () { + this.getAll = function () { + return CRM['civicase-features'].currencyCodes; + }; + + this.getSymbol = function (name) { + return CRM['civicase-features'] + .currencyCodes + .filter(currency => currency.name === name) + .pop().symbol || '£'; + }; + + this.getFormat = function (name) { + return CRM['civicase-features'] + .currencyCodes + .filter(currency => currency.name === name) + .pop().format || null; + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase-features/shared/services/feature-case-types.service.js b/ang/civicase-features/shared/services/feature-case-types.service.js new file mode 100644 index 000000000..d2135b229 --- /dev/null +++ b/ang/civicase-features/shared/services/feature-case-types.service.js @@ -0,0 +1,14 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('FeatureCaseTypes', FeatureCaseTypes); + + /** + * FeatureCaseTypes Service + */ + function FeatureCaseTypes () { + this.getCaseTypes = function ($feature) { + return CRM['civicase-features'].featureCaseTypes[$feature] || []; + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase-features/shared/services/sales-order-status.service.js b/ang/civicase-features/shared/services/sales-order-status.service.js new file mode 100644 index 000000000..fa3d2983e --- /dev/null +++ b/ang/civicase-features/shared/services/sales-order-status.service.js @@ -0,0 +1,21 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('SalesOrderStatus', SalesOrderStatus); + + /** + * SalesOrderStatus Service + */ + function SalesOrderStatus () { + this.getAll = function () { + return CRM['civicase-features'].salesOrderStatus; + }; + + this.getValueByName = function (name) { + return CRM['civicase-features'] + .salesOrderStatus + .filter(status => status.name === name) + .pop().value || ''; + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase/shared/directives/history-back.directive.js b/ang/civicase/shared/directives/history-back.directive.js new file mode 100644 index 000000000..463bb1cc4 --- /dev/null +++ b/ang/civicase/shared/directives/history-back.directive.js @@ -0,0 +1,20 @@ +(function (angular, $window) { + var module = angular.module('civicase'); + + module.directive('historyBack', function () { + return { + restrict: 'A', + link: function (scope, elem, attrs) { + elem.bind('click', function () { + $window.history.back(); + const currPage = window.location.href; + setTimeout(function () { + if ($window.location.href === currPage) { + $window.close(); + } + }, 500); + }); + } + }; + }); +})(angular, window); diff --git a/api/v3/Case/Getdetails.php b/api/v3/Case/Getdetails.php index 6bcd2739e..9fc88c8a2 100644 --- a/api/v3/Case/Getdetails.php +++ b/api/v3/Case/Getdetails.php @@ -6,6 +6,7 @@ */ require_once 'api/v3/Case.php'; +require_once 'api/v3/CaseType.php'; use CRM_Civicase_APIHelpers_CaseDetails as CaseDetailsQuery; /** @@ -217,6 +218,16 @@ function civicrm_api3_case_getdetails(array $params) { $case['related_case_ids'] = CRM_Case_BAO_Case::getRelatedCaseIds($case['id']); } } + + // Get case type category. + if (in_array('case_type_category', $toReturn)) { + foreach ($result['values'] as $id => &$case) { + $caseType = civicrm_api3_case_type_get(['id' => $case['case_type_id']]); + if (!empty($caseType['values']) && is_array($caseType['values'])) { + $case['case_type_category'] = $caseType['values'][$case['case_type_id']]['case_type_category']; + } + } + } if (!empty($params['sequential'])) { $result['values'] = array_values($result['values']); } diff --git a/api/v3/CaseSalesOrder/Get.php b/api/v3/CaseSalesOrder/Get.php new file mode 100644 index 000000000..f31813127 --- /dev/null +++ b/api/v3/CaseSalesOrder/Get.php @@ -0,0 +1,44 @@ + $field) { + $spec[$fieldname] = $field; + } +} diff --git a/api/v3/CaseSalesOrder/Getlist.php b/api/v3/CaseSalesOrder/Getlist.php new file mode 100644 index 000000000..612cc9499 --- /dev/null +++ b/api/v3/CaseSalesOrder/Getlist.php @@ -0,0 +1,88 @@ + 3, + 'entity' => 'CaseSalesOrder', + 'action' => 'getlist', + 'params' => $params, + ]; + + return civicrm_api3_generic_getList($apiRequest); +} + +/** + * Get case sales order list output. + * + * @param array $result + * API Result to format. + * @param array $request + * API Request. + * + * @return array + * API Result + * + * @see _civicrm_api3_generic_getlist_output + */ +function _civicrm_api3_case_sales_order_getlist_output($result, $request) { + $output = []; + if (!empty($result['values'])) { + foreach ($result['values'] as $row) { + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('contact.display_name', 'quotation_date') + ->addJoin('Contact AS contact', 'LEFT', ['contact.id', '=', 'client_id']) + ->addWhere('id', '=', $row['id']) + ->execute() + ->first(); + + $data = [ + 'id' => $row[$request['id_field']], + 'label' => "ID: {$caseSalesOrder['id']}, Client: {$caseSalesOrder['contact.display_name']}", + 'description' => [ + strip_tags($row['description']), + ], + ]; + $data['description'][] = "Quotation Date: " . CRM_Utils_Date::customFormat($caseSalesOrder['quotation_date']); + $output[] = $data; + } + } + return $output; +} diff --git a/civicase.civix.php b/civicase.civix.php index c1e8efe5b..5a4fb9350 100644 --- a/civicase.civix.php +++ b/civicase.civix.php @@ -24,7 +24,7 @@ class CRM_Civicase_ExtensionUtil { * Translated text. * @see ts */ - public static function ts($text, $params = []) { + public static function ts($text, $params = []): string { if (!array_key_exists('domain', $params)) { $params['domain'] = [self::LONG_NAME, NULL]; } @@ -41,7 +41,7 @@ public static function ts($text, $params = []) { * Ex: 'http://example.org/sites/default/ext/org.example.foo'. * Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'. */ - public static function url($file = NULL) { + public static function url($file = NULL): string { if ($file === NULL) { return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/'); } @@ -84,40 +84,17 @@ public static function findClass($suffix) { * * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config */ -function _civicase_civix_civicrm_config(&$config = NULL) { +function _civicase_civix_civicrm_config($config = NULL) { static $configured = FALSE; if ($configured) { return; } $configured = TRUE; - $template =& CRM_Core_Smarty::singleton(); - - $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR; - $extDir = $extRoot . 'templates'; - - if (is_array($template->template_dir)) { - array_unshift($template->template_dir, $extDir); - } - else { - $template->template_dir = [$extDir, $template->template_dir]; - } - + $extRoot = __DIR__ . DIRECTORY_SEPARATOR; $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); set_include_path($include_path); -} - -/** - * (Delegated) Implements hook_civicrm_xmlMenu(). - * - * @param $files array(string) - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu - */ -function _civicase_civix_civicrm_xmlMenu(&$files) { - foreach (_civicase_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) { - $files[] = $file; - } + // Based on , this does not currently require mixin/polyfill.php. } /** @@ -127,35 +104,7 @@ function _civicase_civix_civicrm_xmlMenu(&$files) { */ function _civicase_civix_civicrm_install() { _civicase_civix_civicrm_config(); - if ($upgrader = _civicase_civix_upgrader()) { - $upgrader->onInstall(); - } -} - -/** - * Implements hook_civicrm_postInstall(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall - */ -function _civicase_civix_civicrm_postInstall() { - _civicase_civix_civicrm_config(); - if ($upgrader = _civicase_civix_upgrader()) { - if (is_callable([$upgrader, 'onPostInstall'])) { - $upgrader->onPostInstall(); - } - } -} - -/** - * Implements hook_civicrm_uninstall(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall - */ -function _civicase_civix_civicrm_uninstall() { - _civicase_civix_civicrm_config(); - if ($upgrader = _civicase_civix_upgrader()) { - $upgrader->onUninstall(); - } + // Based on , this does not currently require mixin/polyfill.php. } /** @@ -163,212 +112,9 @@ function _civicase_civix_civicrm_uninstall() { * * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable */ -function _civicase_civix_civicrm_enable() { - _civicase_civix_civicrm_config(); - if ($upgrader = _civicase_civix_upgrader()) { - if (is_callable([$upgrader, 'onEnable'])) { - $upgrader->onEnable(); - } - } -} - -/** - * (Delegated) Implements hook_civicrm_disable(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable - * @return mixed - */ -function _civicase_civix_civicrm_disable() { +function _civicase_civix_civicrm_enable(): void { _civicase_civix_civicrm_config(); - if ($upgrader = _civicase_civix_upgrader()) { - if (is_callable([$upgrader, 'onDisable'])) { - $upgrader->onDisable(); - } - } -} - -/** - * (Delegated) Implements hook_civicrm_upgrade(). - * - * @param $op string, the type of operation being performed; 'check' or 'enqueue' - * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks - * - * @return mixed - * based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) - * for 'enqueue', returns void - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade - */ -function _civicase_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { - if ($upgrader = _civicase_civix_upgrader()) { - return $upgrader->onUpgrade($op, $queue); - } -} - -/** - * @return CRM_Civicase_Upgrader - */ -function _civicase_civix_upgrader() { - if (!file_exists(__DIR__ . '/CRM/Civicase/Upgrader.php')) { - return NULL; - } - else { - return CRM_Civicase_Upgrader_Base::instance(); - } -} - -/** - * Search directory tree for files which match a glob pattern. - * - * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored. - * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles() - * - * @param string $dir base dir - * @param string $pattern , glob pattern, eg "*.txt" - * - * @return array - */ -function _civicase_civix_find_files($dir, $pattern) { - if (is_callable(['CRM_Utils_File', 'findFiles'])) { - return CRM_Utils_File::findFiles($dir, $pattern); - } - - $todos = [$dir]; - $result = []; - while (!empty($todos)) { - $subdir = array_shift($todos); - foreach (_civicase_civix_glob("$subdir/$pattern") as $match) { - if (!is_dir($match)) { - $result[] = $match; - } - } - if ($dh = opendir($subdir)) { - while (FALSE !== ($entry = readdir($dh))) { - $path = $subdir . DIRECTORY_SEPARATOR . $entry; - if ($entry[0] == '.') { - } - elseif (is_dir($path)) { - $todos[] = $path; - } - } - closedir($dh); - } - } - return $result; -} - -/** - * (Delegated) Implements hook_civicrm_managed(). - * - * Find any *.mgd.php files, merge their content, and return. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed - */ -function _civicase_civix_civicrm_managed(&$entities) { - $mgdFiles = _civicase_civix_find_files(__DIR__, '*.mgd.php'); - sort($mgdFiles); - foreach ($mgdFiles as $file) { - $es = include $file; - foreach ($es as $e) { - if (empty($e['module'])) { - $e['module'] = E::LONG_NAME; - } - if (empty($e['params']['version'])) { - $e['params']['version'] = '3'; - } - $entities[] = $e; - } - } -} - -/** - * (Delegated) Implements hook_civicrm_caseTypes(). - * - * Find any and return any files matching "xml/case/*.xml" - * - * Note: This hook only runs in CiviCRM 4.4+. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes - */ -function _civicase_civix_civicrm_caseTypes(&$caseTypes) { - if (!is_dir(__DIR__ . '/xml/case')) { - return; - } - - foreach (_civicase_civix_glob(__DIR__ . '/xml/case/*.xml') as $file) { - $name = preg_replace('/\.xml$/', '', basename($file)); - if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) { - $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name)); - throw new CRM_Core_Exception($errorMessage); - } - $caseTypes[$name] = [ - 'module' => E::LONG_NAME, - 'name' => $name, - 'file' => $file, - ]; - } -} - -/** - * (Delegated) Implements hook_civicrm_angularModules(). - * - * Find any and return any files matching "ang/*.ang.php" - * - * Note: This hook only runs in CiviCRM 4.5+. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules - */ -function _civicase_civix_civicrm_angularModules(&$angularModules) { - if (!is_dir(__DIR__ . '/ang')) { - return; - } - - $files = _civicase_civix_glob(__DIR__ . '/ang/*.ang.php'); - foreach ($files as $file) { - $name = preg_replace(':\.ang\.php$:', '', basename($file)); - $module = include $file; - if (empty($module['ext'])) { - $module['ext'] = E::LONG_NAME; - } - $angularModules[$name] = $module; - } -} - -/** - * (Delegated) Implements hook_civicrm_themes(). - * - * Find any and return any files matching "*.theme.php" - */ -function _civicase_civix_civicrm_themes(&$themes) { - $files = _civicase_civix_glob(__DIR__ . '/*.theme.php'); - foreach ($files as $file) { - $themeMeta = include $file; - if (empty($themeMeta['name'])) { - $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file)); - } - if (empty($themeMeta['ext'])) { - $themeMeta['ext'] = E::LONG_NAME; - } - $themes[$themeMeta['name']] = $themeMeta; - } -} - -/** - * Glob wrapper which is guaranteed to return an array. - * - * The documentation for glob() says, "On some systems it is impossible to - * distinguish between empty match and an error." Anecdotally, the return - * result for an empty match is sometimes array() and sometimes FALSE. - * This wrapper provides consistency. - * - * @link http://php.net/glob - * @param string $pattern - * - * @return array - */ -function _civicase_civix_glob($pattern) { - $result = glob($pattern); - return is_array($result) ? $result : []; + // Based on , this does not currently require mixin/polyfill.php. } /** @@ -387,8 +133,8 @@ function _civicase_civix_insert_navigation_menu(&$menu, $path, $item) { if (empty($path)) { $menu[] = [ 'attributes' => array_merge([ - 'label' => CRM_Utils_Array::value('name', $item), - 'active' => 1, + 'label' => $item['name'] ?? NULL, + 'active' => 1, ], $item), ]; return TRUE; @@ -452,37 +198,3 @@ function _civicase_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) } } } - -/** - * (Delegated) Implements hook_civicrm_alterSettingsFolders(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders - */ -function _civicase_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { - $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; - if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) { - $metaDataFolders[] = $settingsDir; - } -} - -/** - * (Delegated) Implements hook_civicrm_entityTypes(). - * - * Find any *.entityType.php files, merge their content, and return. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes - */ -function _civicase_civix_civicrm_entityTypes(&$entityTypes) { - $entityTypes = array_merge($entityTypes, [ - 'CRM_Civicase_DAO_CaseCategoryInstance' => [ - 'name' => 'CaseCategoryInstance', - 'class' => 'CRM_Civicase_DAO_CaseCategoryInstance', - 'table' => 'civicrm_case_category_instance', - ], - 'CRM_Civicase_DAO_CaseContactLock' => [ - 'name' => 'CaseContactLock', - 'class' => 'CRM_Civicase_DAO_CaseContactLock', - 'table' => 'civicase_contactlock', - ], - ]); -} diff --git a/civicase.php b/civicase.php index aa381a5b8..44e527c15 100644 --- a/civicase.php +++ b/civicase.php @@ -28,7 +28,7 @@ function civicase_civicrm_tabset($tabsetName, &$tabs, $context) { if ($useAng) { $loader = Civi::service('angularjs.loader'); - $loader->addModules('civicase'); + $loader->addModules(['civicase', 'civicase-features']); } } @@ -79,15 +79,16 @@ function civicase_civicrm_config(&$config) { 'hook_civicrm_buildAsset', ['CRM_Civicase_Event_Listener_AssetBuilder', 'addWordReplacements'] ); -} -/** - * Implements hook_civicrm_xmlMenu(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu - */ -function civicase_civicrm_xmlMenu(&$files) { - _civicase_civix_civicrm_xmlMenu($files); + Civi::dispatcher()->addListener( + 'civi.token.list', + ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'listSalesOrderTokens'] + ); + + Civi::dispatcher()->addListener( + 'civi.token.eval', + ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'evaluateSalesOrderTokens'] + ); } /** @@ -99,24 +100,6 @@ function civicase_civicrm_install() { _civicase_civix_civicrm_install(); } -/** - * Implements hook_civicrm_postInstall(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall - */ -function civicase_civicrm_postInstall() { - _civicase_civix_civicrm_postInstall(); -} - -/** - * Implements hook_civicrm_uninstall(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall - */ -function civicase_civicrm_uninstall() { - _civicase_civix_civicrm_uninstall(); -} - /** * Implements hook_civicrm_enable(). * @@ -126,61 +109,6 @@ function civicase_civicrm_enable() { _civicase_civix_civicrm_enable(); } -/** - * Implements hook_civicrm_disable(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable - */ -function civicase_civicrm_disable() { - _civicase_civix_civicrm_disable(); -} - -/** - * Implements hook_civicrm_upgrade(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade - */ -function civicase_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { - return _civicase_civix_civicrm_upgrade($op, $queue); -} - -/** - * Implements hook_civicrm_managed(). - * - * Generate a list of entities to create/deactivate/delete when this module - * is installed, disabled, uninstalled. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed - */ -function civicase_civicrm_managed(&$entities) { - _civicase_civix_civicrm_managed($entities); -} - -/** - * Implements hook_civicrm_caseTypes(). - * - * Generate a list of case-types. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes - */ -function civicase_civicrm_caseTypes(&$caseTypes) { - _civicase_civix_civicrm_caseTypes($caseTypes); -} - -/** - * Implements hook_civicrm_angularModules(). - * - * Generate a list of Angular modules. - * - * Note: This hook only runs in CiviCRM 4.5+. It may - * use features only available in v4.6+. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes - */ -function civicase_civicrm_angularModules(&$angularModules) { - _civicase_civix_civicrm_angularModules($angularModules); -} - /** * Implements hook_civicrm_alterMenu(). * @@ -196,15 +124,6 @@ function civicase_civicrm_alterMenu(&$items) { $items['civicrm/export/standalone']['ids_arguments']['json'][] = 'civicase_reload'; } -/** - * Implements hook_civicrm_alterSettingsFolders(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders - */ -function civicase_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { - _civicase_civix_civicrm_alterSettingsFolders($metaDataFolders); -} - /** * Implements hook_civicrm_buildForm(). */ @@ -230,6 +149,12 @@ function civicase_civicrm_buildForm($formName, &$form) { new CRM_Civicase_Hook_BuildForm_MakePdfFormSubjectRequired(), new CRM_Civicase_Hook_BuildForm_PdfFormButtonsLabelChange(), new CRM_Civicase_Hook_BuildForm_AddScriptToCreatePdfForm(), + new CRM_Civicase_Hook_BuildForm_AddCaseCategoryFeaturesField(), + new CRM_Civicase_Hook_BuildForm_AddQuotationsNotesToContributionSettings(), + new CRM_Civicase_Hook_BuildForm_AddSalesOrderLineItemsToContribution(), + new CRM_Civicase_Hook_BuildForm_AddEntityReferenceToCustomField(), + new CRM_Civicase_Hook_BuildForm_AttachQuotationToInvoiceMail(), + new CRM_Civicase_Hook_BuildForm_RefreshInvoiceListOnUpdate(), ]; foreach ($hooks as $hook) { @@ -300,6 +225,22 @@ function civicase_civicrm_buildForm($formName, &$form) { if (!empty($_REQUEST['civicase_reload'])) { $form->civicase_reload = json_decode($_REQUEST['civicase_reload'], TRUE); } + + $isSearchKit = CRM_Utils_Request::retrieve('sk', 'Positive'); + if ($formName == 'CRM_Contribute_Form_Task_PDF' && $isSearchKit) { + $form->add('hidden', 'mail_task_from_sk', $isSearchKit); + } + + if ($formName == 'CRM_Contribute_Form_Task_Invoice' && $isSearchKit) { + $form->add('hidden', 'mail_task_from_sk', $isSearchKit); + CRM_Core_Resources::singleton()->addScriptFile( + CRM_Civicase_ExtensionUtil::LONG_NAME, + 'js/invoice-bulk-mail.js', + ); + $form->setTitle(ts('Email Contribution Invoice')); + $ids = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE); + $form->assign('totalSelectedContributions', count(explode(',', $ids))); + } } /** @@ -323,6 +264,8 @@ function civicase_civicrm_validateForm($formName, &$fields, &$files, &$form, &$e */ function civicase_civicrm_post($op, $objectName, $objectId, &$objectRef) { $hooks = [ + new CRM_Civicase_Hook_Post_CaseSalesOrderPayment(), + new CRM_Civicase_Hook_Post_CreateSalesOrderContribution(), new CRM_Civicase_Hook_Post_PopulateCaseCategoryForCaseType(), new CRM_Civicase_Hook_Post_CaseCategoryCustomGroupSaver(), new CRM_Civicase_Hook_Post_UpdateCaseTypeListForCaseCategoryCustomGroup(), @@ -333,6 +276,19 @@ function civicase_civicrm_post($op, $objectName, $objectId, &$objectRef) { } } +/** + * Implements hook_civicrm_pre(). + */ +function civicase_civicrm_pre($op, $objectName, $id, &$params) { + $hooks = [ + new CRM_Civicase_Hook_Pre_DeleteSalesOrderContribution(), + ]; + + foreach ($hooks as $hook) { + $hook->run($op, $objectName, $id, $params); + } +} + /** * Implements hook_civicrm_postProcess(). */ @@ -346,6 +302,8 @@ function civicase_civicrm_postProcess($formName, &$form) { new CRM_Civicase_Hook_PostProcess_AttachEmailActivityToAllCases(), new CRM_Civicase_Hook_PostProcess_HandleDraftActivity(), new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryCustomFields(), + new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryFeature(), + new CRM_Civicase_Hook_PostProcess_SaveQuotationsNotesSettings(), ]; foreach ($hooks as $hook) { @@ -356,6 +314,15 @@ function civicase_civicrm_postProcess($formName, &$form) { $api = civicrm_api3('Case', 'getdetails', ['check_permissions' => 1] + $form->civicase_reload); $form->ajaxResponse['civicase_reload'] = $api['values']; } + + if ( + in_array($formName, [ + 'CRM_Contribute_Form_Task_Invoice', 'CRM_Contribute_Form_Task_PDF', + ]) + && !empty($form->getVar('_submitValues')['mail_task_from_sk']) + ) { + CRM_Utils_System::redirect($_SERVER['HTTP_REFERER']); + } } /** @@ -462,6 +429,7 @@ function civicase_civicrm_check(&$messages) { function civicase_civicrm_navigationMenu(&$menu) { $hooks = [ new CRM_Civicase_Hook_NavigationMenu_AlterForCaseMenu(), + new CRM_Civicase_Hook_NavigationMenu_CaseInstanceFeaturesMenu(), ]; foreach ($hooks as $hook) { @@ -490,7 +458,6 @@ function civicase_civicrm_selectWhereClause($entity, &$clauses) { * Implements hook_civicrm_entityTypes(). */ function civicase_civicrm_entityTypes(&$entityTypes) { - _civicase_civix_civicrm_entityTypes($entityTypes); _civicase_add_case_category_case_type_entity($entityTypes); } @@ -594,9 +561,64 @@ function _civicase_add_case_category_case_type_entity(array &$entityTypes) { function civicase_civicrm_alterMailParams(&$params, $context) { $hooks = [ new CRM_Civicase_Hook_alterMailParams_SubjectCaseTypeCategoryProcessor(), + new CRM_Civicase_Hook_alterMailParams_AttachQuotation(), ]; foreach ($hooks as &$hook) { $hook->run($params, $context); } } + +/** + * Implements hook_civicrm_searchKitTasks(). + */ +function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, ?int $userID) { + if (empty($tasks['CaseSalesOrder'])) { + return; + } + + $actions = []; + + if (!empty($tasks['CaseSalesOrder']['delete'])) { + $actions['delete'] = $tasks['CaseSalesOrder']['delete']; + $actions['delete']['title'] = 'Delete Quotation(s)'; + } + + $actions['add_discount'] = [ + 'module' => 'civicase-features', + 'icon' => 'fa-percent', + 'title' => ts('Add Discount'), + 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'], + ]; + + $actions['create_contribution'] = [ + 'module' => 'civicase-features', + 'icon' => 'fa-credit-card', + 'title' => ts('Create Contribution(s)'), + 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html'], + ]; + + $tasks['CaseSalesOrder'] = $actions; + +} + +/** + * Implements hook_civicrm_searchTasks(). + */ +function civicase_civicrm_searchTasks(string $objectName, array &$tasks) { + if ($objectName === 'contribution') { + $tasks['bulk_invoice'] = [ + 'title' => ts('Send Invoice by email'), + 'class' => 'CRM_Contribute_Form_Task_Invoice', + 'icon' => 'fa-paper-plane-o', + 'url' => 'civicrm/contribute/task?reset=1&task_item=invoice&sk=1', + 'key' => 'invoice', + ]; + + foreach ($tasks as &$task) { + if ($task['class'] === 'CRM_Contribute_Form_Task_PDF') { + $task['url'] .= '&sk=1'; + } + } + } +} diff --git a/css/civicase.min.css b/css/civicase.min.css index afda796c1..2fb2b752d 100644 --- a/css/civicase.min.css +++ b/css/civicase.min.css @@ -1,2 +1,2 @@ -@keyframes civicase__infinite-rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}.page-civicrm-case-a .page-title,.page-civicrm-dashboard .page-title,.page-civicrm:not([class*=' page-civicrm-']) .page-title{clip:rect(1px,1px,1px,1px);height:1px;overflow:hidden;position:absolute!important}@font-face{font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff2) format("woff2"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff) format("woff")}#bootstrap-theme .material-icons{direction:ltr;display:inline-block;font-family:'Material Icons';font-feature-settings:'liga';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;letter-spacing:normal;line-height:1;text-rendering:optimizeLegibility;text-transform:none;white-space:nowrap;word-wrap:normal}#bootstrap-theme .badge{font-size:13px;line-height:18px;margin-right:8px;padding-bottom:0;padding-top:0}#bootstrap-theme .badge:last-child{margin-right:0}#bootstrap-theme .btn{font-size:13px;line-height:1.384615em}#bootstrap-theme .crm-clear-link{color:#0071bd}#bootstrap-theme .crm-clear-link .fa-times::before{content:'\f057'}.select2-container.crm-token-selector{width:360px!important}#bootstrap-theme .dropdown-menu{padding:10px 0}#bootstrap-theme .dropdown-menu>li>a{line-height:18px;padding:6px 16px}#bootstrap-theme .dropdown-menu>li .material-icons{color:#9494a5;font-size:13px;margin-right:3px;position:relative;top:2px}#bootstrap-theme .crm_notification-badge{line-height:18px;padding:0 10px}#bootstrap-theme .progress{background:#d3dee2;border-radius:2px;box-shadow:none;height:4px}#bootstrap-theme .select2-container-multi .select2-choices{background:#fff}#bootstrap-theme .select2-container-disabled .select2-choice{background:#f3f6f7;cursor:no-drop;opacity:.8}#bootstrap-theme .simplebar-track{background:#e8eef0;overflow:hidden}#bootstrap-theme .simplebar-track.horizontal{position:relative;height:11px}#bootstrap-theme .simplebar-track.horizontal[style="visibility: hidden;"]{height:0}#bootstrap-theme .simplebar-track.horizontal .simplebar-scrollbar{height:5px;top:3px}#bootstrap-theme .simplebar-scrollbar::before{background:#c2cfd8;border-radius:2.5;opacity:1}#bootstrap-theme .civicase__accordion .panel-heading{padding:0 0 15px}#bootstrap-theme .civicase__accordion .panel-title{font-size:16px}#bootstrap-theme .civicase__accordion .panel-title a,#bootstrap-theme .civicase__accordion .panel-title a:hover{text-decoration:none}#bootstrap-theme .civicase__accordion .panel-title a::before{content:'\f105'}#bootstrap-theme .civicase__accordion.panel-open .panel-title a::before{content:'\f107';margin-left:-4px}#bootstrap-theme .civicase__accordion .panel-body{padding:0 0 15px}#bootstrap-theme .civicase__activities-calendar{transition:opacity .2s linear}#bootstrap-theme .civicase__activities-calendar.is-loading-days{opacity:.7}#bootstrap-theme .civicase__activities-calendar .btn-default,#bootstrap-theme .civicase__activities-calendar table,#bootstrap-theme .civicase__activities-calendar th{background:0 0;color:#464354}#bootstrap-theme .civicase__activities-calendar thead th{position:relative;z-index:1}#bootstrap-theme .civicase__activities-calendar thead tr:nth-child(1){background-color:transparent}#bootstrap-theme .civicase__activities-calendar thead .btn{font-size:16px;text-transform:none}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-title{font-weight:600;margin-top:-3px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-left,#bootstrap-theme .civicase__activities-calendar thead .btn.uib-right{margin-top:-3px;max-width:24px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .material-icons{font-size:24px;line-height:24px}#bootstrap-theme .civicase__activities-calendar .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar .uib-title strong{color:#464354;font-size:16px;font-weight:600;line-height:22px}#bootstrap-theme .civicase__activities-calendar .uib-title span:nth-last-child(1){color:#9494a5}#bootstrap-theme .civicase__activities-calendar [uib-daypicker] .uib-title strong{display:none}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] .civicase__activities-calendar__title-word{display:none}#bootstrap-theme .civicase__activities-calendar tr{background-color:#fff;padding:0 5px}#bootstrap-theme .civicase__activities-calendar tbody,#bootstrap-theme .civicase__activities-calendar tr:nth-child(0n+2) th{background:#fff}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n){border-top-left-radius:5px;border-top-right-radius:5px;margin-top:-3px}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) th{color:#9494a5;font-size:10px;padding:21px 0;text-transform:uppercase}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) .current-week-day{color:#0071bd}#bootstrap-theme .civicase__activities-calendar thead th:nth-child(1){border-top-left-radius:2px}#bootstrap-theme .civicase__activities-calendar thead th:nth-last-child(1){border-top-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-child(1){border-bottom-left-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-last-child(1){border-bottom-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tbody{border-bottom-left-radius:5px;border-bottom-right-radius:5px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);min-height:205px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] thead,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] thead{padding-bottom:7px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] tbody,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] tbody{margin-top:-3px;min-height:262px}#bootstrap-theme .civicase__activities-calendar tbody .btn{font-weight:600;padding:10px 0;position:relative;width:100%}#bootstrap-theme .civicase__activities-calendar .btn.active{background-color:#cde1ed;color:#0071bd}#bootstrap-theme .civicase__activities-calendar .uib-day .btn.active{height:28px;margin-top:2px;padding:0;width:28px}#bootstrap-theme .civicase__activities-calendar .uib-day .material-icons{display:none}#bootstrap-theme .civicase__activities-calendar__day-status.uib-day .material-icons{color:#9494a5;display:block;font-size:6px;left:50%;position:absolute;transform:translateX(-50%) translateY(3px);width:6px}#bootstrap-theme .civicase__activities-calendar__day-status--completed.uib-day .material-icons{color:#44cb7e}#bootstrap-theme .civicase__activities-calendar__day-status--overdue.uib-day .material-icons{color:#cf3458}#bootstrap-theme .civicase__activities-calendar__day-status--scheduled.uib-day .material-icons{color:#0071bd}#bootstrap-theme .activities-calendar-popover{border-color:#e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);margin-top:15px;max-width:280px;padding:0}#bootstrap-theme .activities-calendar-popover>.arrow{border-bottom-color:#e8eef0}#bootstrap-theme .activities-calendar-popover .popover-content{max-height:330px;overflow-x:hidden;overflow-y:auto;padding:0}#bootstrap-theme .activities-calendar-popover__footer{border-top:1px solid #e8eef0}#bootstrap-theme .activities-calendar-popover__see-all{padding:10px}#bootstrap-theme .civicase__activities-calendar__dropdown{transform:translateX(calc(-100% + 18px))}#bootstrap-theme .civicase__activity-card--big{display:flex;flex-direction:column;height:auto;min-height:264px;width:100%}#bootstrap-theme .civicase__activity-card--big .panel{flex-grow:1}#bootstrap-theme .civicase__activity-card--big .panel .panel-body{padding:16px 24px 24px}#bootstrap-theme .civicase__activity-card--big .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--big .material-icons{vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-menu{top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-type{flex:1 0 0;font-size:16px;line-height:22px;margin-bottom:12px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date{color:#4d4d69}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date .material-icons{font-size:22px;margin-right:5px;position:relative;top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card .civicase__checkbox{margin-left:2px;margin-right:10px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-additional__container--avatar{margin-left:5px;margin-top:1px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--big .civicase__contact-icon{margin-top:-3px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row{align-items:flex-start}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row.civicase__activity-card-row--first{border-bottom:1px solid #e8eef0;margin:0 -24px 15px;padding:1px 16px 16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container{align-items:center;display:flex;justify-content:center;width:auto}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container span:not(.civicase__activity-icon-ribbon){margin:0 8px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:62px;left:16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:2px;vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__tags-container{margin-bottom:10px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-subject{color:#9494a5;font-size:13px;font-weight:400;line-height:18px;margin:5px 0 17px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-attachment__container,#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{position:relative}#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{margin-left:4px}#bootstrap-theme .civicase__activity-card--big--empty{align-items:center;display:flex;flex-direction:column;justify-content:center;min-height:265px;text-align:center;width:100%}#bootstrap-theme .civicase__activity-card--big--empty.civicase__activity-card--big--empty--list-view{border:1px solid #d3dee2;border-radius:2px;min-height:265px}#bootstrap-theme .civicase__activity-card--big--empty-title{font-size:20px;font-weight:600;line-height:27px;margin:15px 0 5px}#bootstrap-theme .civicase__activity-card--big--empty-description{color:#9494a5;margin-bottom:20px;padding:0 5px}#bootstrap-theme .civicase__activity-card--big--empty-button{border-color:#0071bd!important;font-size:13px;font-weight:600;line-height:18px;padding:10px 16px}#bootstrap-theme .civicase__activity-card--big--empty-button,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:focus{background-color:inherit}#bootstrap-theme .civicase__activity-card--big--empty-button .material-icons{color:#0071bd;margin-right:8px;position:relative;top:-1px}#bootstrap-theme .civicase__activity-card--big--empty-button i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__activity-card--big--empty-button.btn-default:active,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:hover,#bootstrap-theme .civicase__activity-card--big--empty-button:hover .material-icons,#bootstrap-theme .civicase__activity-card--big--empty-button[disabled]:hover{background-color:#0071bd;color:#fff}#bootstrap-theme .civicase__activity-card--long{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);position:relative}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:63px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:12px;top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--first{margin-bottom:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container--ribbon{width:50px}#bootstrap-theme .civicase__activity-card--long .civicase__checkbox{margin-left:10px;margin-right:8px}#bootstrap-theme .civicase__activity-card--long .civicase__tooltip{flex:1;max-width:300px;min-width:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-type{display:block;font-size:16px;margin-right:12px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options{display:inline-block}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options .civicase__activity-card-menu.btn-group>.dropdown-menu{transform:translateX(0)}#bootstrap-theme .civicase__activity-card--long .civicase__tags-container{margin-right:5px;margin-top:-3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-star{position:relative;top:3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date{margin-left:5px;margin-top:-1px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__with-year,#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{vertical-align:middle}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{display:none}#bootstrap-theme .civicase__activity-card--long .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-subject{color:#9494a5;font-weight:400;margin-left:30px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-menu.btn-group .btn{margin-left:0;top:-2px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon{min-height:75px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{min-height:70px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .civicase__activity-subject{margin-left:52px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--with-checkbox .civicase__activity-subject{margin-left:64px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft .panel-footer{border-top:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-arrow{left:22px;top:8px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card__case-type{overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication>div{align-items:center;display:flex}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication .civicase__contact-card{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--short{box-shadow:0 1px 4px 0 rgba(49,40,40,.2);min-height:100px;position:relative;width:280px}#bootstrap-theme .civicase__activity-card--short .panel-body{min-height:100px}#bootstrap-theme .civicase__activity-card--short .civicase__contact-avatar{margin-top:-10px}#bootstrap-theme .civicase__activity-card--short .civicase__activity-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card--short .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--short .civicase__activity-card__case-type{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card{background:#fff;border-radius:5px;cursor:pointer}#bootstrap-theme .civicase__activity-card:hover{background:#f3f6f7}#bootstrap-theme .civicase__activity-card .panel{box-shadow:none;height:100%;margin-bottom:0}#bootstrap-theme .civicase__activity-card .panel-body{background:0 0;border-top:0!important;height:100%;padding:15px}#bootstrap-theme .civicase__activity-card .panel-footer{background:0 0;padding:5px 16px}#bootstrap-theme .civicase__activity-card .civicase__contact-avatar{margin-left:5px}#bootstrap-theme .civicase__activity-card .civicase__checkbox{margin-right:5px;margin-top:2px}#bootstrap-theme .civicase__activity-card .panel-footer:hover{background:#e8eef0}#bootstrap-theme .civicase__activity-card .panel-footer:hover>a:hover{text-decoration:none}#bootstrap-theme .civicase__activity-card-inner{position:relative;width:100%}#bootstrap-theme .civicase__activity-card--empty .panel-body{align-items:center;display:flex;justify-content:center}#bootstrap-theme .civicase__activity-card--draft{border:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--alert{background:#fbf0e2;border:1px solid #e6ab5e}#bootstrap-theme .civicase__activity-card--alert:hover{background:#fbf0e2;border-color:#c2cfd8}#bootstrap-theme .civicase__activity-card--alert .civicase__activity-subject{color:#4d4d69;white-space:initial}#bootstrap-theme .civicase__activity-card--alert .civicase__tags-container{margin-left:30px;margin-top:2px}#bootstrap-theme .civicase__activity-card--file .civicase__activity-subject{color:#464354;font-size:16px;font-weight:600;margin-left:0}#bootstrap-theme .civicase__activity-card__case-id__label,#bootstrap-theme .civicase__activity-card__case-id__value,#bootstrap-theme .civicase__activity-card__case-type{color:#9494a5}#bootstrap-theme .civicase__activity-card__case-id__value{font-weight:600}#bootstrap-theme .civicase__activity-icon-container{color:#0071bd;font-size:18px;width:30px}#bootstrap-theme .civicase__activity-icon-container--ribbon{width:38px}#bootstrap-theme .civicase__activity-icon-container--ribbon .civicase__activity-icon{color:#fff;font-size:16px;left:8px;position:relative;top:-6px;z-index:1}#bootstrap-theme .civicase__activity-icon-arrow{font-size:10px;left:24px;position:absolute;top:11px}#bootstrap-theme .civicase__activity-icon-ribbon{border-bottom:6px solid transparent;border-left:14px solid #0071bd;border-radius:2px;border-right:14px solid #0071bd;height:42px;left:17px;position:absolute;top:-3px;width:0}#bootstrap-theme .civicase__activity-icon-ribbon.text-danger{border-left:14px solid #cf3458;border-right:14px solid #cf3458}#bootstrap-theme .civicase__activity-icon-ribbon.civicase__text-success{border-left:14px solid #44cb7e;border-right:14px solid #44cb7e}#bootstrap-theme .civicase__activity-date{color:#9494a5}#bootstrap-theme .civicase__activity__right-container{margin-left:auto;white-space:nowrap}#bootstrap-theme .civicase__activity__right-container>*{display:inline-block!important;vertical-align:middle}#bootstrap-theme .civicase__activity-type{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-type--completed{text-decoration:line-through}#bootstrap-theme .civicase__activity-subject{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-card-row{align-items:flex-start;display:flex;line-height:1.8em;vertical-align:middle}#bootstrap-theme .civicase__activity-card-row--first{margin-bottom:5px}#bootstrap-theme .civicase__activity-card-row--file{display:block;margin-left:30px}#bootstrap-theme .civicase__activity-card-row--case-info{line-height:2em;white-space:nowrap}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-card{margin-right:5px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__pipe{margin:0 5px}#bootstrap-theme .civicase__activity-with{color:#9494a5}#bootstrap-theme .civicase__activity-star{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-star.active{color:#e6ab5e}#bootstrap-theme .civicase__activity-attachment__container a{display:flex!important}#bootstrap-theme .civicase__activity-attachment__container.open .dropdown-toggle{box-shadow:none}#bootstrap-theme .civicase__activity-attachment__container:hover .dropdown-menu{display:block}#bootstrap-theme .civicase__activity-attachment__dropdown-menu{z-index:1061}#bootstrap-theme .civicase__activity-attachment__file-name{color:#0071bd!important}#bootstrap-theme .civicase__activity-attachment__file-description{color:#9494a5}#bootstrap-theme .civicase__activity-attachment__icon{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-attachment__icon:hover{color:#0071bd}#bootstrap-theme .civicase__activity-card-menu .material-icons{vertical-align:initial}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn{border:0;height:18px;margin-left:6px;padding:0;top:0;width:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn .material-icons{font-size:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn.dropdown-toggle{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card-menu.btn-group>.dropdown-menu{left:50%!important;top:150%!important;transform:translateX(-100%)}#bootstrap-theme .civicase__activity-attachment__file-icon{color:#9494a5}#bootstrap-theme .civicase__activity-attachment-load{padding:10px 20px!important}#bootstrap-theme .civicase__activity-attachment-load-icon{animation:civicase__infinite-rotation 2s linear reverse;font-size:16px;margin-top:3px;position:relative;top:3px}#bootstrap-theme .civicase__activity-empty-message{color:#9494a5;font-size:16px;text-align:center}#bootstrap-theme .civicase__activity-empty-link{display:block;text-align:center}#bootstrap-theme .civicase__activity-no-result-icon{background-position:center center;background-repeat:no-repeat;background-size:contain;height:48px;width:48px}#bootstrap-theme .civicase__activity-no-result-icon--milestone{background-image:url(../resources/icons/milestone.svg)}#bootstrap-theme .civicase__activity-no-result-icon--activity{background-image:url(../resources/icons/activities.svg)}#bootstrap-theme .civicase__activity-no-result-icon--case{background-image:url(../resources/icons/cases.svg)}#bootstrap-theme .civicase__activity-no-result-icon--communications{background-image:url(../resources/icons/comms.svg);width:66px}#bootstrap-theme .civicase__activity-no-result-icon--tasks{background-image:url(../resources/icons/tasks.svg)}#bootstrap-theme .civicase__activity-feed{box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__activity-feed .panel-body{background:0 0;border-top:0!important;padding:15px}#bootstrap-theme .civicase__activity-feed>.panel-body{padding-bottom:0;padding-top:8px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header{margin:auto}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header>.panel-body{background:0 0;border-top:0;box-shadow:none;padding:0}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before{color:#464354!important;padding-left:35px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::before{font-size:14px;height:20px;line-height:20px;top:8px;width:20px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message{margin:0 85px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__activity-feed__activity-container{display:inline-block;margin-left:5px;width:calc(100% - 50px)}#bootstrap-theme .civicase__activity-feed__list{padding-left:10px;padding-right:10px;padding-top:6px;position:relative}#bootstrap-theme .civicase__activity-feed__list.active{background:#b3d5ec;border-radius:5px}#bootstrap-theme .civicase__activity-feed__list.civicase__animated-checkbox-card--expanded{padding-left:50px}#bootstrap-theme .civicase__activity-feed__list__vertical_bar::before{background-color:#c2cfd8;bottom:1px;content:'';left:17px;position:absolute;top:50px;width:8px;z-index:1}#bootstrap-theme .civicase__activity-feed__list-item{display:inline-block;position:relative;width:100%}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card{display:inline-block;margin-top:2px;position:relative;vertical-align:top;z-index:2}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar{height:40px;line-height:30px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image img{height:40px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar__full-name{height:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image:hover .civicase__contact-avatar__full-name{left:39px}#bootstrap-theme .civicase__activity-feed__list-item .civicase__activity-card{margin-bottom:5px;margin-left:auto;margin-right:auto;width:100%}#bootstrap-theme .civicase__checkbox--bulk-action{display:inline-block;margin-right:20px;top:13px;vertical-align:top}#bootstrap-theme .civicase__activity-feed-pager .material-icons{font-size:28px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__more>.btn,#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__no-more>.btn{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__spinner{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager__no-more>.btn{background:0 0;border:0;font-weight:600}#bootstrap-theme .civicase__activity-feed__body{display:flex;justify-content:center;margin:auto;max-width:1330px}#bootstrap-theme .civicase__activity-feed__body__list{flex-grow:1;max-width:630px;min-height:400px;overflow:auto}#bootstrap-theme .civicase__activity-feed__body__details{box-sizing:content-box;min-height:400px;min-width:550px;overflow-y:auto;padding-left:15px;padding-right:10px;padding-top:8px;width:550px}#bootstrap-theme .civicase__activity-feed__body__month-nav{margin-left:15px;min-height:400px;overflow-x:hidden;overflow-y:auto;width:125px}#bootstrap-theme .civicase__activity-feed__placeholder{margin-left:auto;margin-right:auto;width:50%}#bootstrap-theme .civicase__activity-feed__placeholder .civicase__panel-transparent-header{width:100%}@media (max-width:1300px){#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__with-year{display:none}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__without-year{display:inline-block}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{padding:15px 10px 15px 15px}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__tags-container .badge{max-width:35px}#bootstrap-theme .civicase__activity-feed__body__list--details-visible .civicase__contact-name{max-width:40px}}#bootstrap-theme .civicase__activity-filter{background:#e8eef0;padding:16px 40px;width:100%;z-index:11}#bootstrap-theme .civicase__activity-filter__settings .dropdown-toggle{background:0 0!important;box-shadow:none!important;padding:0}#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu{width:250px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li{padding:0 20px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li label,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li label{font-weight:400;margin-left:5px;position:relative;top:3px}#bootstrap-theme .civicase__activity-filter__add .material-icons,#bootstrap-theme .civicase__activity-filter__settings .material-icons{color:#9494a5;font-size:20px;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__add .caret,#bootstrap-theme .civicase__activity-filter__settings .caret{line-height:20px;margin-left:4px;position:relative;top:-5px}#bootstrap-theme .civicase__activity-filter__add,#bootstrap-theme .civicase__activity-filter__others{min-width:150px}#bootstrap-theme .civicase__activity-filter__add .select2-container,#bootstrap-theme .civicase__activity-filter__others .select2-container{height:auto!important;max-width:170px;min-width:170px}#bootstrap-theme .civicase__activity-filter__contact{margin-left:8px}#bootstrap-theme .civicase__activity-filter__contact .btn{border:1px solid #c2cfd8!important}#bootstrap-theme .civicase__activity-filter__contact .btn.active{background:#f3f6f7;box-shadow:inset 0 0 5px 0 rgba(0,0,0,.1);color:#0071bd}#bootstrap-theme .civicase__activity-filter__timeline{width:auto}#bootstrap-theme .civicase__activity-filter__case-type-categories{display:inline-block;margin-left:5px;width:175px}#bootstrap-theme .civicase__activity-filter__category{vertical-align:top;width:175px}#bootstrap-theme .civicase__activity-filter__category .crm-i{color:#4d4d69}#bootstrap-theme .civicase__activity-filter__category .select2-chosen{max-width:130px}#bootstrap-theme .civicase__activity-filter__category,#bootstrap-theme .civicase__activity-filter__timeline{display:inline-block;margin-left:8px}#bootstrap-theme .civicase__activity-filter__category .select2-choice .select2-arrow,#bootstrap-theme .civicase__activity-filter__timeline .select2-choice .select2-arrow{top:0;width:24px}#bootstrap-theme .civicase__activity-filter__attachment,#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{background:0 0!important;padding:5px;text-transform:initial}#bootstrap-theme .civicase__activity-filter__attachment .material-icons,#bootstrap-theme .civicase__activity-filter__more .material-icons,#bootstrap-theme .civicase__activity-filter__star .material-icons{color:#9494a5;font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__activity-filter__attachment.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__more.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#0071bd}#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{padding-left:0}#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#e6ab5e}#bootstrap-theme .civicase__activity-filter__more span{color:#4d4d69;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__more-container{margin-top:15px}#bootstrap-theme .civicase__activity-filter__more-container>*{display:inline-block;margin-bottom:15px;margin-left:5px;margin-right:5px;vertical-align:top}#bootstrap-theme .civicase__activity-filter__custom .civicase__activity-filter__header{border-bottom:1px solid #e8eef0;display:block;margin-top:5px}@media (max-width:1420px){#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter{padding:16px 10px}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__timeline{width:120px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__category{width:165px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more__text{display:none}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__attachment,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__star{padding:5px 2px}}#bootstrap-theme .civicase__activity-month-nav{overflow:hidden;width:105px}#bootstrap-theme .civicase__activity-month-nav.affix{position:fixed!important}#bootstrap-theme .civicase__activity-month-nav__group{border-left:2px solid #d9e1e6}#bootstrap-theme .civicase__activity-month-nav__group-month,#bootstrap-theme .civicase__activity-month-nav__group-title,#bootstrap-theme .civicase__activity-month-nav__group-year{padding-left:15px}#bootstrap-theme .civicase__activity-month-nav__group-title{color:#464354;font-weight:700;text-transform:uppercase}#bootstrap-theme .civicase__activity-month-nav__group-year{color:#464354}#bootstrap-theme .civicase__activity-month-nav__group-gap{height:10px}#bootstrap-theme .civicase__activity-month-nav__group-month{color:#9494a5;cursor:pointer;font-weight:600}#bootstrap-theme .civicase__activity-month-nav__group-month.active{border-left:2px solid #0071bd;color:#0071bd;margin-left:-2px}#bootstrap-theme .civicase__activity-month-nav__group-month:hover{color:#0071bd}#bootstrap-theme .civicase__overdue-activity-icon{color:#cf3458!important;display:inline-block;font-weight:600;padding-right:20px;position:relative}#bootstrap-theme .civicase__overdue-activity-icon::before{background-color:#cf3458;content:''}#bootstrap-theme .civicase__overdue-activity-icon::after{color:#fff;content:'!';top:1px}#bootstrap-theme .civicase__overdue-activity-icon::after,#bootstrap-theme .civicase__overdue-activity-icon::before{border-radius:50%!important;font-size:11px;height:13px;line-height:1em;position:absolute;right:0;text-align:center;top:50%;transform:translateY(-50%);width:13px;z-index:0!important}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before{padding-left:20px;padding-right:0}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::before{left:2px;right:auto}#bootstrap-theme .civicase__activity-panel.affix{position:fixed!important}#bootstrap-theme .civicase__activity-panel .panel{overflow:auto;position:relative}#bootstrap-theme .civicase__activity-panel .panel-heading,#bootstrap-theme .civicase__activity-panel .panel-subheading{position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel .panel-subheading{border-bottom:1px solid #e8eef0;top:63px}#bootstrap-theme .civicase__activity-panel .panel-body{margin-bottom:76px;margin-top:120px;overflow:auto;padding:0}#bootstrap-theme .civicase__activity-panel .panel-subtitle{color:#464354;display:flex;font-size:16px;font-weight:600;line-height:25px}#bootstrap-theme .civicase__activity-panel .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-date{font-size:13px;font-weight:400;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-star{position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-panel__close,#bootstrap-theme .civicase__activity-panel__maximise{color:#464354;font-size:18px;padding:0}#bootstrap-theme .civicase__activity-panel__maximise{margin-right:5px;transform:rotate(45deg)}#bootstrap-theme .civicase__activity-panel__status-dropdown{margin-right:5px}#bootstrap-theme .civicase__activity-panel__status-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__priority-dropdown{margin-right:10px}#bootstrap-theme .civicase__activity-panel__priority-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__id{line-height:33px}#bootstrap-theme .civicase__activity-panel__resume-draft{bottom:20px;height:36px;position:absolute;right:20px}#bootstrap-theme .civicase__activity-panel__core_container{min-height:200px;position:static!important}#bootstrap-theme .civicase__activity-panel__core_container .help{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-activity-form-block-separation{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-block{overflow:auto;padding-top:20px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2)>*,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2)>*{margin-bottom:10px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-radio,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-radio{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel,#bootstrap-theme .civicase__activity-panel__core_container .form-layout{box-shadow:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr{border:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .label{padding-left:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .view-value{padding-right:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label label{color:#9494a5!important;font-weight:400!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .section-shown{padding:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-button_qf_Activity_cancel{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons{border-bottom:0!important;border-top:1px solid #e8eef0;bottom:0;height:auto!important;margin:0;padding:20px!important;position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit{color:#fff!important;background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus{color:#fff!important;background-color:#00538a;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{color:#fff!important;background-color:#00538a;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle{color:#fff!important;background-color:#00538a;background-image:none;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:hover{color:#fff!important;background-color:#003d66;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel .badge,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit .badge{color:#0071bd;background-color:#fff!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete{color:#fff;background-color:#cf3458;border-color:#bd2d4e;background-color:#cf3458!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus{color:#fff;background-color:#a82846;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{color:#fff;background-color:#a82846;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle{color:#fff;background-color:#a82846;background-image:none;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:hover{color:#fff;background-color:#8b213a;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{background-color:#cf3458;border-color:#bd2d4e}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete .badge{color:#cf3458;background-color:#fff}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date-wrapper{display:inline-block}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date{width:153px}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-time{margin-left:10px;width:75px}#bootstrap-theme .civicase__activity-panel__core_container .crm-ajax-select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container input[name=followup_activity_subject]{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value>input,#bootstrap-theme .civicase__activity-panel__core_container .view-value>select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value .crm-form-date-wrapper{margin-bottom:10px}.civicase__badge{border-radius:10px;color:#fff;display:inline-block;font-size:13px;line-height:18px;padding:0 7px}.civicase__badge.text-dark{color:#000}.civicase__badge--default{background:#fff;box-shadow:0 0 0 1px #d3dee2 inset;color:#000}#bootstrap-theme .civicase__bulkactions-checkbox{background:#fff;border:1px solid #c2cfd8;border-radius:2px;display:inline-block;padding:0 3px;position:relative}#bootstrap-theme .civicase__bulkactions-checkbox-toggle{color:#e8eef0;cursor:pointer;font-size:18px;margin-left:3px;transition:.3s color cubic-bezier(0,0,0,.4);vertical-align:middle}#bootstrap-theme .civicase__bulkactions-checkbox-toggle.civicase__checkbox{display:inline-block}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked{color:#0071bd;transition-property:color}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked--hide{color:#c2cfd8;font-size:19.5px;left:3px;top:2px}#bootstrap-theme .civicase__bulkactions-select-mode-dropdown{background:#fff;padding:4px 5px;vertical-align:middle}#bootstrap-theme .civicase__bulkactions-actions-dropdown{margin-left:10px;position:relative}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn{border:1px solid #c2cfd8;line-height:18px;padding:5px 25px 5px 10px;text-transform:unset}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn:hover{border-color:#c2cfd8!important}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn+.dropdown-toggle{padding:5px}#bootstrap-theme .civicase__bulkactions-message .alert{background-color:#f3f6f7;border:1px solid #d3dee2;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);line-height:18px;margin-bottom:0;padding:15px;text-align:center}#bootstrap-theme .civicase__checkbox--bulk-action .civicase__checkbox--checked{color:#0071bd}#bootstrap-theme .civicase__button--with-shadow{box-shadow:0 3px 18px 0 rgba(48,40,40,.25)}#bootstrap-theme .civicase__case-activity-count__popover{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;white-space:nowrap;z-index:11}#bootstrap-theme .civicase__case-activity-count__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-activity-count__popover .arrow.left{left:20px}#bootstrap-theme .civicase__case-body{padding:0}#bootstrap-theme .civicase__case-body .tab-content{background:0 0;position:relative;z-index:0}#bootstrap-theme .civicase__case-body_tab{position:relative;z-index:1}#bootstrap-theme .civicase__case-body_tab.affix{position:fixed;top:0;z-index:11}#bootstrap-theme .civicase__case-body_tab.affix+.tab-content{padding-top:50px}#bootstrap-theme .civicase__case-body_tab>[civicase-dropdown]{opacity:1}#bootstrap-theme .civicase__case-details-panel--summary .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--summary .civicase__case-body_tab.affix{width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--focused .civicase__case-body_tab.affix{width:100%}#bootstrap-theme .civicase__case-card{border-radius:0!important;box-shadow:none;cursor:pointer;height:100%;margin-bottom:0}#bootstrap-theme .civicase__case-card:hover{background:#d9edf7}#bootstrap-theme .civicase__case-card .panel-body{background-color:transparent;border:0!important}#bootstrap-theme .civicase__case-card .civicase__contact-card{font-size:14px;font-weight:600;line-height:18px}#bootstrap-theme .civicase__case-card .civicase__contact-card>span{display:flex}#bootstrap-theme .civicase__case-card .civicase__contact-icon{color:#c2cfd8;font-size:24px;margin-top:-3px}#bootstrap-theme .civicase__case-card .civicase__checkbox{left:20px;top:15px}#bootstrap-theme .civicase__case-card .civicase__tags-container .badge{max-width:195px}#bootstrap-theme .civicase__case-card--closed{background-image:repeating-linear-gradient(60deg,#e8eef0,#e8eef0 2px,#f3f6f7 2px,#f3f6f7 20px);min-height:149px}#bootstrap-theme .civicase__case-card--closed .civicase__case-card-subject,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__type,#bootstrap-theme .civicase__case-card--closed .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--closed .civicase__contact-name{text-decoration:line-through}#bootstrap-theme .civicase__case-card--closed .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__next-milestone-date{color:inherit;font-weight:400}#bootstrap-theme .civicase__case-card--case-list .civicase__case-card__activity-info{overflow:hidden;white-space:nowrap}#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name,#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name-additional{max-width:100px}#bootstrap-theme .civicase__case-card--other{border-bottom:1px solid #e8eef0;min-height:auto}#bootstrap-theme .civicase__case-card--other .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--other .civicase__contact-name{color:#464354;font-size:16px}#bootstrap-theme .civicase__case-card--other .civicase__contact-name{max-width:none}#bootstrap-theme .civicase__case-card--other .civicase__case-card__activity-info{display:inline-flex}#bootstrap-theme .civicase__case-card--other .civicase__case-card__next-milestone{margin-right:30px}#bootstrap-theme .civicase__case-card__right_container{color:#9494a5}#bootstrap-theme .civicase__case-card__dates{margin-right:16px;vertical-align:text-top}#bootstrap-theme .civicase__case-card__link-type{font-size:16px;margin-left:16px;vertical-align:middle}#bootstrap-theme .civicase__case-card--active{border-bottom:1px solid #0071bd!important;border-right:1px solid #0071bd!important;border-top:1px solid #0071bd!important}#bootstrap-theme .civicase__case-card__additional-information{line-height:normal;position:absolute;right:20px;top:15px}#bootstrap-theme .civicase__case-card__case-id{color:#9494a5}#bootstrap-theme .civicase__case-card__lock{color:#c2cfd8;font-size:24px;line-height:0;position:relative;top:5px}#bootstrap-theme .civicase__case-card__type{color:#4d4d69}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__contact,#bootstrap-theme .civicase__case-card__next-milestone,#bootstrap-theme .civicase__case-card__type{margin-bottom:3px}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__next-milestone{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card__next-milestone-date{color:#0071bd;font-weight:600}#bootstrap-theme .civicase__case-card__activity-count:hover,#bootstrap-theme .civicase__case-card__next-milestone-date:hover{text-decoration:none}#bootstrap-theme .civicase__case-card__activity-count--zero{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count-container{display:inline-block;margin-right:8px}#bootstrap-theme .civicase__case-card--detached{background:#fff;border-radius:2px!important;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);color:#9494a5;margin-bottom:10px}#bootstrap-theme .civicase__case-card--detached>.panel-body{padding:0}#bootstrap-theme .civicase__case-card--detached .civicase__case-card__date{color:#4d4d69}#bootstrap-theme .civicase__case-card--detached .civicase__contact-icon{font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__case-card--detached .crm_notification-badge{vertical-align:unset}#bootstrap-theme .civicase__case-card--detached .civicase__case-card-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed{background:#f3f6f7;min-height:auto}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-role,#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-subject{color:inherit}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--active{box-shadow:0 0 0 5px #b3d5ec}#bootstrap-theme .civicase__case-card-role-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-card-role-container .material-icons{margin-right:5px}#bootstrap-theme .civicase__case-card-role,#bootstrap-theme .civicase__case-card-subject{color:#4d4d69;line-height:18px}#bootstrap-theme .civicase__case-card__row{border-bottom:1px solid #e8eef0;clear:both}#bootstrap-theme .civicase__case-card__row:last-child{border-bottom:0}#bootstrap-theme .civicase__case-card__row--primary{padding:15px}#bootstrap-theme .civicase__case-card__row--secondary{padding:10px 15px}#bootstrap-theme .civicase__case-custom-fields__container.civicase__summary-tab-tile{padding-top:0}#bootstrap-theme .civicase__case-custom-fields__container civicase-masonry-grid-item:not(:first-child){margin-top:30px}#bootstrap-theme .civicase__case-custom-fields__container .panel-body{border-top:0!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-editable-enabled:not(.crm-editable-editing):hover{border:2px dashed transparent;padding:24px}#bootstrap-theme .civicase__case-custom-fields__container .crm-case-custom-form-block table{width:100%}#bootstrap-theme .civicase__case-custom-fields__container .crm-submit-buttons{border:0;padding:15px 8px 0;text-align:right}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit{min-width:auto;padding:7px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel{padding:6px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel:hover{color:#fff!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container{background:#fff;padding:20px}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container .crm-block{box-shadow:none}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row label{color:#9494a5;font-weight:400!important}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row:not(:first-child){display:block;margin-top:16px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:first-child{display:block;text-align:left}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2){display:block;margin-left:7px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .cke,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) textarea{width:calc(100% - 4px)}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .select2-arrow{padding-right:20px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-radio{margin-right:8px;margin-top:-4px}#bootstrap-theme .civicase__case-details__add-new-dropdown{left:-20px;position:relative;top:6px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary{border:1px solid #fff;line-height:20px;padding:7px 16px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-child(1){margin-right:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown]{position:relative}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu{left:auto;right:0;width:180px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown] .dropdown-menu{top:0}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu .fa-fw,#bootstrap-theme .civicase__case-details__add-new-dropdown a .material-icons{color:#0071bd}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu{max-height:260px;overflow-x:hidden;overflow-y:scroll;padding:8px 0 0;right:170px;top:0;width:220px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control{margin:0 auto;width:204px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control-feedback{color:#464354;font-size:14px;margin-right:8px;margin-top:7px;position:absolute}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu a{padding:9px 17px;white-space:normal}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters a{padding:9px 17px 9px 38px}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters .fa-fw{margin-left:-21px;margin-right:4px}#bootstrap-theme [civicase-dropdown]{position:relative}#bootstrap-theme [civicase-dropdown] .dropdown-menu{top:calc(100% + 8px)}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list{margin-left:auto;margin-right:auto;width:685px}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list::before{content:none}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message{margin:0 85px 10px}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__file-tab-filters{display:inline-block;float:right}#bootstrap-theme .civicase__file-filters-container{display:flex}#bootstrap-theme .civicase__file-filters{margin-left:16px;width:180px}#bootstrap-theme .civicase__file-filters .select2-container{height:auto!important}#bootstrap-theme .civicase__file-filters:not(:nth-child(2)) .form-control:not(.select2-container){width:180px}#bootstrap-theme .civicase__file-filters .input-group-addon{font-size:14px;padding:0;width:30px!important}#bootstrap-theme .civicase__file-filters .input-group-addon .material-icons{position:relative;top:2px}#bootstrap-theme .civicase__case-header{background:#fff;position:relative}#bootstrap-theme .civicase__case-header__expand_button{background:0 0;border-left:1px solid #e8eef0;border-right:1px solid #e8eef0;bottom:0;color:#c2cfd8;font-size:30px;left:0;padding:0;position:absolute;top:0;width:56px}#bootstrap-theme .civicase__case-header__expand_button>.material-icons{vertical-align:middle}#bootstrap-theme .civicase__case-header__content{border-top:1px solid #d3dee2;padding:15px 15px 15px 80px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client{color:#464354;font-size:24px;font-weight:600}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-icon{font-size:30px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-additional__arrow{top:-18px}#bootstrap-theme .civicase__case-header__content .civicase__contact-name{margin-top:1px;max-width:300px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager{display:inline-block;position:relative;top:4px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__case-header__case-type+.civicase__pipe{margin-right:5px}#bootstrap-theme .civicase__case-header__content .civicase__tags-container{position:relative;top:-1px}#bootstrap-theme .civicase__case-header__webform-dropdown+.dropdown-menu>li a{max-width:500px!important}#bootstrap-theme .civicase__case-header__content__first-row{display:flex;min-height:42px}#bootstrap-theme .civicase__case-header__content__trash{font-size:30px;margin-right:5px;position:relative;top:2px;width:20px}#bootstrap-theme .civicase__case-header__case-info,#bootstrap-theme .civicase__case-header__dates{color:#9494a5}#bootstrap-theme .civicase__case-header__case-info{margin-top:5px}#bootstrap-theme .civicase__case-header__case-id,#bootstrap-theme .civicase__case-header__case-source,#bootstrap-theme .civicase__case-header__case-type{color:#464354}#bootstrap-theme .civicase__case-header__case-type a{display:inline}#bootstrap-theme .civicase__case-header__action-menu{position:absolute;right:20px;top:20px}#bootstrap-theme .civicase__case-header__action-menu .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li a{max-width:200px;overflow:hidden;position:relative;text-overflow:ellipsis}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu{left:auto;margin-top:-40px;position:absolute;right:100%}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu a{max-width:500px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li:hover>.dropdown-menu.sub-menu{display:block;opacity:1;visibility:visible}#bootstrap-theme .civicase__case-header__action-icon{font-size:20px;padding:2px 10px}#bootstrap-theme .civicase__case-header__action-icon .material-icons{position:relative;top:3px}#bootstrap-theme .civicase__case-tab--linked-cases .civicase__summary-tab__other-cases{margin-left:0;margin-right:0}#bootstrap-theme .civicase__panel-empty{margin-bottom:110px;margin-top:110px;padding:5px;text-align:center}#bootstrap-theme .civicase__panel-empty .fa.fa-big,#bootstrap-theme .civicase__panel-empty .material-icons{color:#9494a5;font-size:64px}#bootstrap-theme .civicase__panel-empty .empty-label{color:#9494a5;font-size:14px;font-weight:600;line-height:19px;padding:18px 0;text-align:center}#bootstrap-theme .civicase__case-list-table-container{border-left:1px solid #e8eef0;margin-left:300px;overflow-x:auto;overflow-y:visible}#bootstrap-theme .civicase__case-list-table{table-layout:inherit}#bootstrap-theme .civicase__case-list-table td:first-child,#bootstrap-theme .civicase__case-list-table th:first-child{width:300px}#bootstrap-theme .civicase__case-list-table th{line-height:18px;min-width:142px;padding:22px 15px!important}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-checkbox{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown .civicase__bulkactions-actions-dropdown__text{width:60px}#bootstrap-theme .civicase__case-list-table .civicase__case-list-column--first{padding:16px 14px!important}#bootstrap-theme .civicase__case-list-table th:first-child{background:#f3f6f7;left:0;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-table th:nth-child(2){max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table tr{height:150px}#bootstrap-theme .civicase__case-list-table thead tr{height:63px}#bootstrap-theme .civicase__case-list-table td{height:150px;min-width:142px;padding:20px}#bootstrap-theme .civicase__case-list-table td:first-child{background:#fff;left:0;padding:0;position:absolute;width:300px;z-index:1}#bootstrap-theme .civicase__case-list-table td:nth-child(2){vertical-align:middle}#bootstrap-theme .civicase__case-list-table .case-activity-card-wrapper{max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table__column--status_badge{max-width:200px;min-width:200px!important}#bootstrap-theme .civicase__case-list-table__column--status_badge .crm_notification-badge{display:block;max-width:fit-content;overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__case-list-table__header.affix{display:block;left:0;margin-left:300px;overflow-x:hidden;overflow-y:visible;right:0;top:60px;z-index:10}#bootstrap-theme .civicase__case-list-table__header.affix tr{display:table;width:100%}#bootstrap-theme .civicase__case-list-table__header.affix th{border-bottom:1px solid #e8eef0;display:table-cell}#bootstrap-theme .civicase__case-list-table__header.affix th:nth-child(1){left:0;position:fixed}#bootstrap-theme .civicase__case-list{margin:0;overflow:hidden;position:relative}#bootstrap-theme .civicase__case-list .civicase__bulkactions-message .alert{border-bottom:0}#bootstrap-theme .civicase__case-list .civicase__pager--fixed{position:fixed}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case{width:300px}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case.civicase__pager--fixed{position:absolute}#bootstrap-theme .civicase__case-list .civicase__pager--case-focused{display:none}#bootstrap-theme .civicase__case-list--summary>.civicase__pager{bottom:0;height:60px;position:absolute}#bootstrap-theme .civicase__case-list--summary .civicase__case-list-table-container{overflow-x:hidden}#bootstrap-theme .civicase__case-list-panel{border-top:1px solid #d3dee2;box-shadow:none;margin-bottom:0;overflow:auto;padding:0;position:relative;transition:width .3s linear}#bootstrap-theme .civicase__case-list-panel--summary{border-top:0;bottom:60px;overflow-x:hidden;position:absolute;top:65px;width:300px}#bootstrap-theme .civicase__case-list-panel--summary thead{display:none}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-column--first{background:#fff!important}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-table td:first-child{width:100%}#bootstrap-theme .civicase__case-list-column--first--detached{background:#fff;border-bottom:1px solid #d3dee2;border-top:1px solid #d3dee2;height:65px;left:0;padding:16px 14px;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-panel--focused{width:0}#bootstrap-theme .civicase__case-list-panel--focused .civicase__pager{display:none}#bootstrap-theme .civicase__case-details-panel{box-shadow:none;display:none;float:right;height:0;overflow:hidden;transition:width .3s;width:0}#bootstrap-theme .civicase__case-details-panel>.panel-body{background:0 0}#bootstrap-theme .civicase__case-details-panel .civicase__panel-empty{background:#fff;height:100%;margin:0;padding:15px 20px}#bootstrap-theme .civicase__case-details-panel--summary{display:block;height:100%;overflow-y:auto;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused{width:100%}#bootstrap-theme .civicase__case-list-sortable-header{cursor:pointer}#bootstrap-theme .civicase__case-list-sortable-header:hover{background-color:#e8eef0}#bootstrap-theme .civicase__case-list-sortable-header.active{background-color:#e8eef0!important}#bootstrap-theme .civicase__case-list__toggle-sort{color:#9494a5;cursor:pointer;font-size:20px;position:relative;top:7px}#bootstrap-theme .civicase__case-list__header-toggle-sort{float:right;position:relative;top:3px}#bootstrap-theme .civicase__case-sort-dropdown{box-shadow:none;display:inline-block;width:90px!important}#bootstrap-theme .civicase__case-overview .panel-body{background-color:#fafafb;padding:0!important}#bootstrap-theme .civicase__case-overview paging{padding-right:20px}#bootstrap-theme .civicase__case-overview-container a{color:inherit;text-decoration:none}#bootstrap-theme .civicase__case-overview-container .civicase__case-overview__flow,#bootstrap-theme .civicase__case-overview-container .simplebar-content{position:static}#bootstrap-theme .civicase__case-overview-container .simplebar-content{padding-right:0!important}#bootstrap-theme .civicase__case-overview__breakdown,#bootstrap-theme .civicase__case-overview__flow{display:flex;margin-left:200px}#bootstrap-theme .civicase__case-overview__breakdown-field,#bootstrap-theme .civicase__case-overview__flow-status{display:inline-flex;flex-basis:200px;flex-grow:1;flex-shrink:0}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child,#bootstrap-theme .civicase__case-overview__flow-status:first-child{align-items:flex-start;align-items:center;flex-direction:row;justify-content:flex-start;left:0;position:absolute;top:auto;width:200px;z-index:5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child .civicase__case-overview__flow-status__icon,#bootstrap-theme .civicase__case-overview__flow-status:first-child .civicase__case-overview__flow-status__icon{color:#0071bd;cursor:pointer;margin-left:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-overview__flow-status{align-items:flex-start;background-color:#fff;flex-direction:column;height:80px;justify-content:center;position:relative}#bootstrap-theme .civicase__case-overview__flow-status::after{background-color:#fff;border-bottom-right-radius:5px;box-shadow:1px 1px 0 0 #e8eef0;content:'';height:57px;position:absolute;right:-25px;top:12px;transform:rotateZ(-45deg);transform-origin:50% 50%;width:57px;z-index:1}#bootstrap-theme .civicase__case-overview__flow-status:first-child{font-size:16px;font-weight:600;line-height:22px;padding-left:24px;z-index:10}#bootstrap-theme .civicase__case-overview__flow-status:last-child{overflow:hidden}#bootstrap-theme .civicase__case-overview__flow-status:last-child::after{content:none}#bootstrap-theme .civicase__case-overview__flow-status-settings{position:relative}#bootstrap-theme .civicase__case-overview__flow-status-settings .btn{color:inherit;margin-right:10px;padding:3px 0;text-decoration:none!important}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings{color:inherit!important;margin:0!important;vertical-align:middle}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings.material-icons{color:#9494a5!important;font-size:18px}#bootstrap-theme .civicase__case-overview__flow-status__border{bottom:0;height:4px;position:absolute;transform:skewx(-45deg);width:calc(100% - 1px);z-index:2}#bootstrap-theme .civicase__case-overview__flow-status__count{color:#464354;font-size:24px;font-weight:600;line-height:33px;text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__empty-state{text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__description{color:#9494a5;margin:0 auto;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:140px}#bootstrap-theme .civicase__case-overview__flow-status__description span{text-overflow:ellipsis}#bootstrap-theme .civicase__case-overview__breakdown:last-child .civicase__case-overview__breakdown-field{border:0}#bootstrap-theme .civicase__case-overview__breakdown-field{align-items:center;border-bottom:1px solid #e8eef0;justify-content:center;padding:16px 24px;position:relative}#bootstrap-theme .civicase__case-overview__breakdown-field:not(:first-child){color:#9494a5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after,#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{background-color:#fafafb;bottom:-1px;content:'';height:1px;position:absolute;width:24px}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child{background-color:#fafafb;font-weight:600}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child a{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after{left:0}#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{right:0}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover::after,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover::after{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:0;z-index:1}#bootstrap-theme .civicase__case-overview__popup.dropdown-menu{margin-top:15px;padding:8px 0}#bootstrap-theme .civicase__case-overview__popup .popover-content{padding:0}#bootstrap-theme .civicase__case-overview__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-overview__popup .arrow.left{left:20px}#bootstrap-theme .civicase__case-overview__popup .dropdown-menu{box-shadow:none;display:inherit;position:inherit}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox{display:inline-block;margin-right:5px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox--checked{color:#0071bd!important;font-size:24px!important;top:0!important}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container{line-height:18px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-tab--people .nav-tabs{border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03)}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox{margin-right:16px}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox .civicase__people-tab__table-checkbox{cursor:pointer;height:100%;left:0;opacity:0;position:absolute;right:0;top:0;width:100%;z-index:11}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab-link{line-height:18px;padding-bottom:15px;padding-top:12px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-header .civicase__people-tab__table-column{line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-body .civicase__people-tab__table-column{padding:16px 20px}#bootstrap-theme .civicase__people-tab{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__people-tab__sub-tab .civicase__add-btn{margin-right:-6px;margin-top:-4px}#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled],#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled]:hover{color:#9494a5;cursor:not-allowed}#bootstrap-theme .civicase__people-tab__search{background:#fff;padding:20px 30px}#bootstrap-theme .civicase__people-tab__search h3{margin:0}#bootstrap-theme .civicase__people-tab__search .btn .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__people-tab__search .dropdown-menu{left:auto!important;right:0;top:100%!important}#bootstrap-theme .civicase__people-tab__search .civicase__bulkactions-actions-dropdown .dropdown-menu{right:auto}#bootstrap-theme .civicase__people-tab__selection{align-items:center;display:flex;padding:16px 0}#bootstrap-theme .civicase__people-tab__selection>input{margin:0 5px 0 9px}#bootstrap-theme .civicase__people-tab__selection label{line-height:18px;margin-bottom:0;position:relative;top:1px}#bootstrap-theme .civicase__people-tab__select-box .form-control{width:240px}#bootstrap-theme .civicase__people-tab__filter{align-items:center;border-bottom:1px solid #e8eef0;border-top:1px solid #e8eef0;display:flex;justify-content:space-between;padding:10px 24px}#bootstrap-theme .civicase__people-tab__filter--role .form-control{width:160px}#bootstrap-theme .civicase__people-tab__filter--relations{justify-content:unset}#bootstrap-theme .civicase__people-tab__filter-alpha-pager{margin-left:20px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link{color:#464354;margin:0 3px;padding:2px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.active,#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.all{color:#0071bd;text-decoration:none}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link:first-child{margin-left:0;padding-left:0}#bootstrap-theme .civicase__people-tab__table-column--first{display:flex}#bootstrap-theme .civicase__people-tab__table-column--first em{font-weight:400}#bootstrap-theme .civicase__people-tab__table-column--first input{margin:0}#bootstrap-theme .civicase__people-tab__table-column--last{padding:20px 0!important}#bootstrap-theme .civicase__people-tab__table-column--last .dropdown-menu{left:auto!important;right:20px;top:100%!important}#bootstrap-theme .civicase__people-tab__table-column--last .btn{padding:0}#bootstrap-theme .civicase__people-tab__table-column--last .open{position:relative}#bootstrap-theme .civicase__people-tab__table-column--last .open .dropdown-toggle{background:0 0!important;box-shadow:none}#bootstrap-theme .civicase__people-tab__table-column--last .material-icons{font-size:18px;padding:0}#bootstrap-theme .civicase__people-tab__inactive-filter{margin-left:auto;margin-right:30px}#bootstrap-theme .civicase__people-tab__inactive-filter .civicase__checkbox{display:inline-block;margin-right:5px;top:5px}#bootstrap-theme .civicase__people-tab__table-assign-icon{cursor:pointer}#bootstrap-theme .civicase__people-tab__table-assign-icon:hover{color:#0071bd}#bootstrap-theme .civicase__people-tab-counter{border-top:1px solid #e8eef0;line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-filter-panel{background-color:#f3f6f7;box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__case-filter-panel .panel-header{position:relative}#bootstrap-theme .civicase__case-filter-panel__title{font-size:18px;left:20px;line-height:24px;margin:0;max-width:calc(((100% - 950px)/ 2) - 20px);overflow:hidden;position:absolute;text-overflow:ellipsis;top:50%;transform:translateY(-50%);white-space:nowrap}#bootstrap-theme .civicase__case-filters-container{display:flex;justify-content:center;left:0;padding:13.5px 0;top:0}#bootstrap-theme .civicase__case-filter__input.form-control{margin:0 8px}#bootstrap-theme .civicase__case-filter__input.form-control:not(.select2-container){width:240px}#bootstrap-theme .civicase__case-filter-panel__button{margin:0 8px;width:158px}#bootstrap-theme .civicase__case-filter-panel__button:first-child{margin-left:0}#bootstrap-theme .civicase__case-filter-panel__button .fa{font-size:18px;margin-right:7px;position:relative;top:2px}#bootstrap-theme .civicase__case-filter-form-elements-container{margin:0 auto;width:926px}#bootstrap-theme .civicase__case-filter-form-elements{clear:both;margin-bottom:10px}#bootstrap-theme .civicase__case-filter-form-elements .select2-choices{padding-right:30px}#bootstrap-theme .civicase__case-filter-form-elements .form-control{max-width:385px}#bootstrap-theme .civicase__case-filter-form-elements.civicase__case-filter-form-elements--case-id .form-control{max-width:160px}#bootstrap-theme .civicase__case-filter-form-elements label,#bootstrap-theme .civicase__case-filter-form-elements-container .civicase__checkbox__container label{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__case-filter-panel__description{align-items:center;display:flex;flex-direction:row}#bootstrap-theme .civicase__filter-search-description-list-container{flex:1 0 0;margin-bottom:0}#bootstrap-theme .civicase__case-filter-form-legend{border-color:#e8eef0;color:#464354;font-size:16px;font-weight:600;line-height:22px;margin-bottom:20px;padding:0 0 8px}#bootstrap-theme .civicase__case-filter-fieldset{margin:15px 0}#bootstrap-theme .civicase__case-summary-fields:not(:first-child){margin-top:16px}#bootstrap-theme .civicase__case-summary-fields__label{color:#9494a5}#bootstrap-theme .civicase__case-summary-fields__value{color:#464354;word-break:break-all}#bootstrap-theme .civicase__case-tab__container{padding:24px 30px}#bootstrap-theme .civicase__case-tab__actions{margin-bottom:16px}#bootstrap-theme .civicase__case-tab__empty{color:#464354;font-weight:600;margin-top:40px;opacity:.65}#bootstrap-theme .civicase__checkbox{background-color:#fff;border:1px solid #c2cfd8;border-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03);box-sizing:border-box;cursor:pointer;display:inline-block;height:18px;margin-right:5px;position:relative;width:18px}#bootstrap-theme .civicase__checkbox__container .control-label{position:relative;top:-4px}#bootstrap-theme .civicase__checkbox--checked{color:#c2cfd8;font-size:24px;left:0;margin-left:-4px;margin-top:-4px;position:absolute;top:0;transition:.2s all cubic-bezier(0,0,0,.4);z-index:10}#bootstrap-theme .civicase__animated-checkbox-card{position:relative;transition:.1s padding-left cubic-bezier(0,0,0,.4);transition-delay:.1s}#bootstrap-theme .civicase__animated-checkbox-card .civicase__checkbox--bulk-action{cursor:pointer;left:14px;opacity:0;outline:0;position:absolute;top:15px;transform:scale(.3);transition:.1s all cubic-bezier(0,0,0,.4);transition-delay:unset}#bootstrap-theme .civicase__animated-checkbox-card--expanded{padding-left:30px;transition-delay:0}#bootstrap-theme .civicase__animated-checkbox-card--expanded .civicase__checkbox--bulk-action{opacity:1;transform:scale(1);transition-delay:.1s}.civicase__contact-activity-tab__add .select2-container .select2-choice{background:#4d4d69;border:0;box-shadow:none;height:auto;line-height:initial;padding:7px 19px;width:155px!important}.civicase__contact-activity-tab__add .select2-container .select2-chosen{color:#fff!important;margin:0;text-transform:uppercase}.civicase__contact-activity-tab__add .select2-container .select2-arrow{background:0 0!important;border:0;line-height:34px}.civicase__contact-activity-tab__add .select2-container .select2-arrow::before{color:#fff!important}#bootstrap-theme .civicase__contact-card{color:#0071bd;display:flex}#bootstrap-theme .civicase__contact-name-container{display:flex}#bootstrap-theme .civicase__contact-name,#bootstrap-theme .civicase__contact-name-additional{color:inherit;margin-left:5px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__contact-icon,#bootstrap-theme .civicase__contact-icon-additional{color:#c2cfd8;font-size:18px;margin-top:2px}#bootstrap-theme .civicase__contact-icon-additional.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon-additional:hover,#bootstrap-theme .civicase__contact-icon.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon:hover{color:#0071bd;cursor:pointer}#bootstrap-theme .civicase__contact-icon .material-icons,#bootstrap-theme .civicase__contact-icon-additional .material-icons{line-height:inherit}#bootstrap-theme .civicase__contact-additional__container{margin-left:8px}#bootstrap-theme .civicase__contact-additional__container,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts{color:#0071bd;padding-left:0!important;padding-right:0!important}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-icon,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-icon{font-size:18px}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-name-additional,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-name-additional{color:#0071bd}#bootstrap-theme .civicase__contact-additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer}#bootstrap-theme .civicase__contact-additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__contact-additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__contact-additional__popover .popover-content{padding:0}#bootstrap-theme .civicase__contact-additional__list{margin:0;padding:0}#bootstrap-theme .civicase__contact-additional__list li{height:34px;list-style:none;padding:7px 20px}#bootstrap-theme .civicase__contact-additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__contact-additional__list li .civicase__contact-icon{vertical-align:middle}#bootstrap-theme .civicase__contact-additional__list li a{text-decoration:none}#bootstrap-theme .civicase__contact-additional__hidden_contacts_info{color:#9494a5;font-size:12px}#bootstrap-theme .civicase__contact-avatar{background:#99c6e5;min-width:30px;padding:5px;position:relative}#bootstrap-theme .civicase__contact-additional__container--avatar{background:#99c6e5;margin-left:0;margin-top:-10px;min-width:30px;padding:5px}#bootstrap-theme .civicase__contact-avatar--image{background:0 0;padding:0;position:relative;z-index:1}#bootstrap-theme .civicase__contact-avatar--image img{border-radius:2px;height:25px;width:25px}#bootstrap-theme .civicase__contact-avatar__full-name{background:#99c6e5;border-radius:1px;display:none;height:30px;left:0;padding:5px 10px;position:absolute;top:0;width:auto}#bootstrap-theme .civicase__contact-avatar--image .civicase__contact-avatar__full-name{border-bottom-left-radius:0;border-top-left-radius:0}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover{opacity:1}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover .civicase__contact-avatar__full-name{display:block}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover.civicase__contact-avatar--image .civicase__contact-avatar__full-name{left:29px;padding-left:11px;z-index:0}#bootstrap-theme .civicase__contact-card__with-more-fields{flex-wrap:wrap}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__more-field{color:#464354}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__break{flex-basis:100%;height:0}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact-name{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields{max-height:300px;overflow-y:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li{height:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li:not(:first-of-type){border-top:1px solid #e8eef0}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact-name-additional{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact__more-field{margin-top:5px}#bootstrap-theme .civicase__contact-cases-tab{margin-left:-5px}#bootstrap-theme .civicase__contact-cases-tab-container{padding-left:15px;padding-right:15px}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:18px;margin:0;padding:15px 0}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-body{background:0 0;box-shadow:none;padding:0}#bootstrap-theme .civicase__contact-case-tab__case-list__footer{margin-top:24px}#bootstrap-theme .civicase__contact-case-tab__case-list__footer .btn{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__contact-cases-tab-empty{align-items:center;display:flex;flex-direction:column;padding:50px 0}#bootstrap-theme .civicase__contact-cases-tab-empty a{color:#4d4d69}#bootstrap-theme .civicase__contact-cases-tab-add{background:#4d4d69;margin-bottom:10px}#bootstrap-theme .civicase__contact-cases-tab-add .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);margin-top:calc((1.1 * 18px) + 2 * 15px)}#bootstrap-theme .civicase__contact-cases-tab-details>.panel-body{padding:0}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group{margin-right:5px}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group:last-child{margin-right:0}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__tags-container{max-width:80%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__activity-card{width:100%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__subject{margin-bottom:3px;margin-left:-2px;margin-top:20px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__description{margin-bottom:5px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client{position:relative;top:8px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client .material-icons,#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager{position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__status-label{display:inline-block;max-width:250px;overflow:hidden;position:relative;text-overflow:ellipsis;top:3px;white-space:nowrap}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__case-link .fa{font-size:17px;margin-right:5px;position:relative;top:1px}#bootstrap-theme .civicase__contact-cases-tab-details .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__contact-cases-tab-details__title{margin:6.5px 0}#bootstrap-theme .civicase__contact-cases-tab__panel-row{border-bottom:1px solid #e8eef0;padding:15px 24px}#bootstrap-theme .civicase__contact-cases-tab__panel-row:last-child{border-bottom:0}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__subject textarea{min-height:65px}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__description textarea{min-height:85px}#bootstrap-theme .civicase__contact-cases-tab__panel-actions{padding:20px}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark{background-color:#f3f6f7}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark .civicase__pipe{color:#e8eef0;margin:0 8px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields{padding-bottom:15px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields--inline{align-items:center;display:flex}#bootstrap-theme .civicase__contact-cases-tab__panel-field-emphasis{color:#9494a5}#bootstrap-theme .civicase__contact-cases-tab__panel-field-title{color:#9494a5;margin-bottom:5px}.crm-contact-page #ui-id-5{padding:30px;width:calc(100% - 200px)}@media (max-width:1400px){#bootstrap-theme .civicase__contact-card--client{clear:both}}.contact-popover-container{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;max-width:90vw;padding:20px;width:708px}.contact-popover-container .popover-content{padding:0}.contact-popover-container.bottom>.arrow{border-bottom-color:#e8eef0}.contact-popover-container.top>.arrow{border-top-color:#e8eef0}.civicase__contact-popover__header h2{line-height:24px;margin:0}.civicase__contact-popover__header hr{background-color:#e8eef0;margin:16px 0}.civicase__contact-popover__column{float:left;width:46%}.civicase__contact-popover__column+.civicase__contact-popover__column{width:54%}.civicase__contact-popover__detail-group{float:left;margin-bottom:10px;width:100%}.civicase__contact-popover__detail-header,.civicase__contact-popover__detail-value{color:#4d4d69;float:left;line-height:18px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;width:55%}.civicase__contact-popover__detail-header strong,.civicase__contact-popover__detail-value strong{color:#464354;font-weight:600}.civicase__contact-popover__detail-header{clear:both;width:45%}#bootstrap-theme .civicrm__contact-prompt-dialog textarea{width:100%}#bootstrap-theme .civicrm__contact-prompt-dialog__date-error.crm-error{background:#fbe3e4}#bootstrap-theme .civicase__crm-dashboard__tabs{position:relative;width:100%;z-index:1}#bootstrap-theme .civicase__crm-dashboard__tabs.affix{position:fixed;z-index:11}#bootstrap-theme .civicase__crm-dashboard__myactivities-tab{padding:0}#bootstrap-theme .civicase__dashboard__tab{background:#e8eef0}#bootstrap-theme .civicase__dashboard__tab>.civicase__dashboard__tab__top{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab .panel-secondary{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__col{padding:0 15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{display:flex;flex-direction:column;margin:0 -15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__main{margin:auto;max-width:1081px;padding:0 30px}@media (min-width:992px){#bootstrap-theme .civicase__dashboard__tab__col--left{flex-basis:330px;flex-grow:0;flex-shrink:0}#bootstrap-theme .civicase__dashboard__tab__col--right{flex-grow:1}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{flex-direction:row}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:0}}@media (min-width:1200px){#bootstrap-theme .civicase__dashboard__tab__main{padding:0}}#bootstrap-theme .civicase__dashboard .tab-pane{padding:0}#bootstrap-theme .civicase__dashboard__tab-container .nav{width:100%;z-index:10}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix-top{position:relative}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix{position:fixed}#bootstrap-theme .civicase__dashboard-activites-feed{background:#e8eef0}#bootstrap-theme .civicase__dashboard__action-btn{background:#0071bd;border:1px solid #fff;border-radius:2px;color:#fff;line-height:20px;margin-right:15px;margin-top:7px;padding:6px 16px}#bootstrap-theme .civicase__dashboard__action-btn .material-icons{font-size:16px;margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__dashboard__action-btn--light{background:#fff;color:#0071bd}#bootstrap-theme .civicase__dashboard__relation-filter{display:inline-block;margin-right:10px;margin-top:7px;width:190px}#bootstrap-theme .civicase__dashboard__relation-filter .select2-choice{color:#9494a5}#bootstrap-theme .civicase__ui-range>span{display:inline-block;margin-right:16px}#bootstrap-theme .civicase__ui-range .crm-form-date{display:inline-block;width:134px}#bootstrap-theme .civicase__ui-range .crm-form-date-wrapper{display:inline-block;position:relative}#bootstrap-theme .civicase__ui-range .crm-clear-link{position:absolute;right:-20px;top:50%;transform:translateY(-50%)}#bootstrap-theme .civicase__activity-panel__core_container--draft [title='File On Case']{display:none}.crm-container.ui-dialog .ui-dialog-content.civicase__email-role-selector{height:220px!important}#bootstrap-theme .civicase__file-upload-container{margin:0 -12px}#bootstrap-theme .civicase__file-upload-item{padding:0 12px}#bootstrap-theme .civicase__file-upload-dropzone{align-items:center;border:1px dashed #c2cfd8;border-radius:3px;display:flex;flex-direction:column;height:330px;justify-content:center;padding:20px;width:100%}#bootstrap-theme .civicase__file-upload-dropzone:hover{background-color:#fff}#bootstrap-theme .civicase__file-upload-dropzone .material-icons{color:#bfcfd9;font-size:48px}#bootstrap-theme .civicase__file-upload-dropzone h3{font-size:14px;line-height:18px;margin:10px 0 0}#bootstrap-theme .civicase__file-upload-dropzone label{color:#0071bd;cursor:pointer;font-weight:400}#bootstrap-theme .civicase__file-upload-button{display:none!important}#bootstrap-theme .civicase__file-upload-box{transition:.25s width cubic-bezier(0,0,0,.4);width:100%}#bootstrap-theme .civicase__file-upload-details{opacity:0;overflow:hidden;padding:0;transition:.1s opacity cubic-bezier(0 0,0,.4);transition-delay:.3s;width:0}#bootstrap-theme .civicase__file-upload-details label{color:#9494a5;font-weight:400;line-height:18px;margin-bottom:5px}#bootstrap-theme .civicase__file-upload-details .btn{margin-right:8px;padding:8px 16px}#bootstrap-theme .civicase__file-upload-details .btn:last-child{margin-right:0}#bootstrap-theme .civicase__file-upload-details .btn-default{border-color:inherit}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector{margin-bottom:25px;margin-top:-10px}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7{float:none}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .select2-container{margin-bottom:5px;padding:0;width:100%!important}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-box{width:50%}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-details{opacity:1;overflow:visible;padding:0 12px;width:50%}#bootstrap-theme .civicase__file-upload-name,#bootstrap-theme .civicase__file-upload-remove,#bootstrap-theme .civicase__file-upload-size{font-size:13px;line-height:18px;margin:0}#bootstrap-theme .civicase__file-upload-name{color:#0071bd}#bootstrap-theme .civicase__file-upload-description{margin-bottom:24px}#bootstrap-theme .civicase__file-upload-description textarea{min-height:90px}#bootstrap-theme .civicase__file-upload-remove{text-align:right}#bootstrap-theme .civicase__file-upload-remove .btn{border:0;color:#cf3458;padding:0;text-transform:capitalize}#bootstrap-theme .civicase__file-upload-remove .btn:hover{background-color:transparent}#bootstrap-theme .civicase__file-upload-progress{margin:12px 0 16px}#bootstrap-theme .civicase__icon{transform:translateY(14%) rotate(.03deg)}#bootstrap-theme .civicase-inline-datepicker__wrapper{margin-left:-11px;margin-top:-5px}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]{border:1px solid transparent;border-radius:2px;height:30px;padding:4px 10px;width:140px!important}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control{box-shadow:none;display:inline-block}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control.ng-invalid{background-color:#fbe3e4}#bootstrap-theme .civicase-inline-datepicker__wrapper .addon{margin-top:-3px!important;opacity:0;transition:opacity .15s}#bootstrap-theme .civicase-inline-datepicker__wrapper:active [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:focus [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:hover [civicase-inline-datepicker]{border:1px solid #c2cfd8}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .form-control{box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .addon{opacity:1}#bootstrap-theme .civicase-inline-datepicker__wrapper .civicase__inline-datepicker--open{border:1px solid #c2cfd8;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:active+.addon,#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:focus+.addon{opacity:1}#bootstrap-theme .civicase__loading-placeholder__icon{color:#edf3f5;display:inline-block;font-size:10px;left:-14px;position:relative;top:-1px;width:10px}#bootstrap-theme .civicase__loading-placeholder__activity-card{border:1px solid #edf3f5;border-radius:4px;height:90px;margin-bottom:10px;position:relative;width:280px}#bootstrap-theme .civicase__loading-placeholder__activity-card::before{background-color:#edf3f5;border:.4em solid #fff;border-radius:1.5em;content:' ';font-size:1.5em;height:2.7em;left:1em;padding-top:.2em;position:absolute;text-align:center;top:.4em;width:2.7em}#bootstrap-theme .civicase__loading-placeholder__activity-card::after{background-color:#edf3f5;content:' ';height:30px;position:absolute;right:20px;top:20px;width:12px}#bootstrap-theme .civicase__loading-placeholder__activity-card div{margin-left:90px;margin-right:90px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::before{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::after{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__oneline::before{background-color:#edf3f5;content:' ';display:block;height:1em}#bootstrap-theme .civicase__loading-placeholder__date{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__date::before{border-left-color:#d6e4e8;border-top-color:#d6e4e8}#bootstrap-theme .civicase__loading-placeholder--big::before{height:1.5em}#bootstrap-theme .panel-header .civicase__loading-placeholder__oneline::before{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__oneline-strip{border-left-color:#edf3f5!important}#bootstrap-theme civicase-masonry-grid{width:100%}#bootstrap-theme civicase-masonry-grid .civicase__masonry-grid__column{float:left;width:50%}#bootstrap-theme civicase-masonry-grid-item{display:block}#bootstrap-theme .civicase__pager{background:#fafafb;border-radius:0 0 3px 3px;border-top:1px solid #e8eef0;padding:18px 15px;position:relative;z-index:10}#bootstrap-theme .civicase__pager .disabled{display:none}#bootstrap-theme .civicase__pager [title='First Page'] a,#bootstrap-theme .civicase__pager [title='Last Page'] a,#bootstrap-theme .civicase__pager [title='Next Page'] a,#bootstrap-theme .civicase__pager [title='Previous Page'] a{font-size:16px;font-weight:400;top:-3px}#bootstrap-theme .civicase__pager--fixed{bottom:0;left:0;position:fixed;width:100%}#bootstrap-theme .panel-query>.panel-body{transition:opacity .2s linear}#bootstrap-theme .panel-query.is-loading-page>.panel-body{opacity:.7}#bootstrap-theme .panel-query .civicase__activity-card--empty{text-align:center}#bootstrap-theme .panel-query .civicase__activity-card--big--empty-description{margin-bottom:0}#bootstrap-theme .panel-query .civicase__activity-no-result-icon{display:inline-block}#bootstrap-theme .panel-secondary{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .panel-secondary>.panel-body{padding:24px}#bootstrap-theme .panel-secondary>.panel-footer{padding:16px 24px}#bootstrap-theme .panel-secondary>.panel-heading{background:#fff;line-height:1;padding:24px;position:relative}#bootstrap-theme .panel-secondary>.panel-heading::after{border-bottom:1px solid #e8eef0;bottom:0;content:'';display:block;height:0;left:16px;position:absolute;width:calc(100% - 32px)}#bootstrap-theme .panel-secondary>.panel-heading .panel-title{font-size:16px}#bootstrap-theme .panel-secondary .panel-heading-control{display:block;margin-left:0;margin-top:-13px;position:relative;top:6px}#bootstrap-theme+.panel-secondary .panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary a.panel-heading-control{line-height:30px}#bootstrap-theme .panel-secondary .panel-title+.panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary .civicase__activity-card--long,#bootstrap-theme .panel-secondary .civicase__case-card--detached{box-shadow:0 3px 12px 0 rgba(49,40,40,.14);margin-bottom:0}#bootstrap-theme .panel-secondary .civicase__activity-card--long:not(:last-child),#bootstrap-theme .panel-secondary .civicase__case-card--detached:not(:last-child){margin-bottom:15px}#bootstrap-theme .civicase__panel-transparent-header{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading{background:0 0;padding:0}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:16px;margin:3px 0 12px}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading h3::before{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .civicase__pipe{color:#c2cfd8;font-weight:400;margin:0 5px}#bootstrap-theme .civicase__panel-transparent-header>.panel-body{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);padding:24px;position:relative}#bootstrap-theme civicase-popover{display:inline-block}#bootstrap-theme .civicase__popover-box{display:block}#bootstrap-theme civicase-popover-toggle-button{cursor:pointer}.civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}.civicase__tooltip-popup-list .popover-content{padding:0}.civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__responsive-calendar tbody,#bootstrap-theme .civicase__responsive-calendar thead{display:block}#bootstrap-theme .civicase__responsive-calendar tr{display:flex}#bootstrap-theme .civicase__responsive-calendar td,#bootstrap-theme .civicase__responsive-calendar th{padding:0;width:100%}#bootstrap-theme .civicase__show-more-button{cursor:pointer}#bootstrap-theme .civicase__summary-tab__basic-details{background:#fff}#bootstrap-theme .civicase__summary-tab__basic-details .panel-body{display:flex;flex-direction:row;padding:20px}#bootstrap-theme .civicase__summary-tab__subject-container{flex-basis:56%;padding-right:15px}#bootstrap-theme .civicase__summary-tab__subject{font-size:20px;font-weight:600;line-height:27px;margin:0 0 13px}#bootstrap-theme .civicase__summary-tab__description{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-tab__last-updated{margin:13px 0 0;padding:2px 4px}#bootstrap-theme .civicase__summary-tab__last-updated__label{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-activity-count{align-items:center;border-left:1px solid #e8eef0;color:#464354;display:table-cell;font-weight:600;justify-content:center;text-align:center;vertical-align:top;width:20%}#bootstrap-theme .civicase__summary-activity-count a,#bootstrap-theme .civicase__summary-activity-count a:hover{color:#464354;text-decoration:none}#bootstrap-theme .civicase__summary-activity-count a{display:block;height:100%;margin:0 10px}#bootstrap-theme .civicase__summary-activity-count a:hover{background:#f3f6f7;border-radius:5px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__number{font-size:50px;line-height:76px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__description{color:#9494a5;font-size:15px;line-height:22px}#bootstrap-theme .civicase__summary-overdue-count{line-height:18px}#bootstrap-theme .civicase__summary-tab-tile{margin-bottom:15px;padding:15px}#bootstrap-theme .civicase__summary-tab-tile>.panel{margin-bottom:0}#bootstrap-theme .civicase__summary-tab-tile>.panel-body{border-radius:5px;border-top:0!important;padding:0}#bootstrap-theme .civicase__summary-tab-tile .civicase__panel-transparent-header>.panel-body{border-radius:5px}#bootstrap-theme .civicase__summary-tab-tile-container{display:flex;padding:0 15px;width:100%}#bootstrap-theme .civicase__summary-tab-tile--fixed{width:350px}#bootstrap-theme .civicase__summary-tab-tile--responsive{flex-basis:calc(50% - 150px);flex-grow:1;min-width:0}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-heading .civicase__case-card__activity-count:nth-child(2){margin-left:10px}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-body{background:0 0;box-shadow:none}#bootstrap-theme .civicase__summary-tab__activity-list .civicase__activity-card{margin-bottom:15px}#bootstrap-theme .civicase__summary-tab__other-cases{margin-left:25px;margin-right:25px}#bootstrap-theme .civicase__summary-tab__other-cases>.panel-collapse>.panel-body{padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .panel-footer{border-top:0;padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager{border-top:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::after,#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::before{display:none}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li[title^=Page].active>a{color:#464354}#crm-container .civicase__tabs{background-color:#fff;padding:0 0 0 20px}#crm-container .civicase__tabs .ui-tab{background:0 0;border:0;padding:0}#crm-container .civicase__tabs .ui-tab .ui-tabs-anchor{height:auto;padding:15px 20px!important}#crm-container .civicase__tabs__panel{padding:15px 20px}#bootstrap-theme .civicase__tags-container .badge,#bootstrap-theme .civicase__tags-container__additional__list .badge{margin-right:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__tags-container{display:inline-block}#bootstrap-theme .civicase__tags-container .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional__list{margin:0;padding:0}#bootstrap-theme .civicase__tags-container__additional__list li{list-style:none;padding:5px 10px}#bootstrap-theme .civicase__tags-container__additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__tags-container__additional__list li a{text-decoration:none}#bootstrap-theme .civicase__tags-container__additional__list .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional-tags{background-color:#9494a5;display:inline;padding:0 8px}#bootstrap-theme .civicase__tags-container__additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer;max-width:calc(36ch + 20px + 4px);padding:0}#bootstrap-theme .civicase__tags-container__additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__tags-container__additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tags-container__additional__popover .popover-content{background:#fff;padding:0}.civicase__tags-modal{background-color:#fff!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container{border:1px solid #d3dee2;border-radius:2px;padding:5px}#bootstrap-theme .civicase__tags-modal__generic-tags-container input{margin-top:0!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container label{margin-bottom:0}#bootstrap-theme .civicase__tags-modal__tags-container label{margin-top:4px}#bootstrap-theme .civicase__tags-modal__tags-container .col-sm-9{width:75%!important}.civicase__tags-selector__item-color{margin-right:2px;position:relative;top:1px}#bootstrap-theme .civicase__tooltip__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;z-index:5}#bootstrap-theme .civicase__tooltip__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tooltip__popup .arrow.left{left:20px}#bootstrap-theme .civicase__tooltip__ellipsis{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__pipe{position:relative;top:-1px}#bootstrap-theme .civicase__text-warning{color:#e6ab5e}#bootstrap-theme .civicase__text-success{color:#44cb7e}#bootstrap-theme .civicase__link-disabled{cursor:no-drop;pointer-events:none}#bootstrap-theme .civicase__spinner{animation:spin 1.5s linear infinite;background:url(../resources/icons/spinner.svg) no-repeat center center!important;display:block;height:32px;margin:auto;width:32px}#bootstrap-theme .civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}#bootstrap-theme .civicase__tooltip-popup-list .popover-content{padding:0}#bootstrap-theme .civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase-workflow-list>.panel-body{padding:0}#bootstrap-theme .civicase-workflow-list__new-button{margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__new-button .material-icons{position:relative;top:2px}#bootstrap-theme .civicase-workflow-list_duplicate-form .ng-invalid:not(.ng-untouched){border-color:#cf3458}#bootstrap-theme .civicase-workflow-list_duplicate-form textarea{width:100%}#bootstrap-theme .civicase-workflow-list__filters .form-group{margin-bottom:0}#bootstrap-theme .civicase-workflow-list__filters .form-group [type=checkbox]{margin:0}#bootstrap-theme .civicase-workflow-list__filters .form-group>div{display:inline-block;margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(odd){margin-right:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(even){margin-left:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group .select2-container{width:240px!important}#civicaseActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)}.page-civicrm-contact-view:not([class*=page-civicrm-contact-view-]) #crm-container .civicase__activity-panel__core_container .crm-submit-buttons{margin-bottom:0!important}.page-civicrm-case-a #page{margin:0;padding-top:0}.page-civicrm-case-a .block-civicrm>h2{margin:0}.page-civicrm-case-a #branding{padding:16px 0!important}.page-civicrm-case-a #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-case-a #branding .breadcrumb>a{left:0}@media (max-width:1455px){.page-civicrm-case-a .crm-contactEmail-form-block-subject .crm-token-selector{margin-top:5px}}.page-civicrm-dashboard #page,.page-civicrm:not([class*=' page-civicrm-']) #page{margin:0;padding-top:0}.page-civicrm-dashboard .block-civicrm>h2,.page-civicrm:not([class*=' page-civicrm-']) .block-civicrm>h2{margin:0}.page-civicrm-dashboard #branding,.page-civicrm:not([class*=' page-civicrm-']) #branding{padding:16px 0!important}.page-civicrm-dashboard #branding .breadcrumb,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-dashboard #branding .breadcrumb>a,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb>a{left:0}.page-civicrm-dashboard .civicase__tabs.affix,.page-civicrm:not([class*=' page-civicrm-']) .civicase__tabs.affix{position:fixed;top:0;width:100%;z-index:9999}.civicase__crm-dashboard .tab-content,.civicase__crm-dashboard.ui-tabs{background:0 0}#civicaseMyActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseMyActivitiesTab [crm-page-title]{display:none}#civicaseMyActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseMyActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseMyActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)} +@keyframes civicase__infinite-rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}.page-civicrm-case-a .page-title,.page-civicrm-dashboard .page-title,.page-civicrm:not([class*=' page-civicrm-']) .page-title{clip:rect(1px,1px,1px,1px);height:1px;overflow:hidden;position:absolute!important}@font-face{font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff2) format("woff2"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff) format("woff")}#bootstrap-theme .material-icons{direction:ltr;display:inline-block;font-family:'Material Icons';font-feature-settings:'liga';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;letter-spacing:normal;line-height:1;text-rendering:optimizeLegibility;text-transform:none;white-space:nowrap;word-wrap:normal}#bootstrap-theme .badge{font-size:13px;line-height:18px;margin-right:8px;padding-bottom:0;padding-top:0}#bootstrap-theme .badge:last-child{margin-right:0}#bootstrap-theme .btn{font-size:13px;line-height:1.384615em}#bootstrap-theme .crm-clear-link{color:#0071bd}#bootstrap-theme .crm-clear-link .fa-times::before{content:'\f057'}.select2-container.crm-token-selector{width:360px!important}#bootstrap-theme .dropdown-menu{padding:10px 0}#bootstrap-theme .dropdown-menu>li>a{line-height:18px;padding:6px 16px}#bootstrap-theme .dropdown-menu>li .material-icons{color:#9494a5;font-size:13px;margin-right:3px;position:relative;top:2px}#bootstrap-theme .crm_notification-badge{line-height:18px;padding:0 10px}#bootstrap-theme .progress{background:#d3dee2;border-radius:2px;box-shadow:none;height:4px}#bootstrap-theme .select2-container-multi .select2-choices{background:#fff}#bootstrap-theme .select2-container-disabled .select2-choice{background:#f3f6f7;cursor:no-drop;opacity:.8}#bootstrap-theme .simplebar-track{background:#e8eef0;overflow:hidden}#bootstrap-theme .simplebar-track.horizontal{position:relative;height:11px}#bootstrap-theme .simplebar-track.horizontal[style="visibility: hidden;"]{height:0}#bootstrap-theme .simplebar-track.horizontal .simplebar-scrollbar{height:5px;top:3px}#bootstrap-theme .simplebar-scrollbar::before{background:#c2cfd8;border-radius:2.5;opacity:1}#bootstrap-theme .civicase__accordion .panel-heading{padding:0 0 15px}#bootstrap-theme .civicase__accordion .panel-title{font-size:16px}#bootstrap-theme .civicase__accordion .panel-title a,#bootstrap-theme .civicase__accordion .panel-title a:hover{text-decoration:none}#bootstrap-theme .civicase__accordion .panel-title a::before{content:'\f105'}#bootstrap-theme .civicase__accordion.panel-open .panel-title a::before{content:'\f107';margin-left:-4px}#bootstrap-theme .civicase__accordion .panel-body{padding:0 0 15px}#bootstrap-theme .civicase__activities-calendar{transition:opacity .2s linear}#bootstrap-theme .civicase__activities-calendar.is-loading-days{opacity:.7}#bootstrap-theme .civicase__activities-calendar .btn-default,#bootstrap-theme .civicase__activities-calendar table,#bootstrap-theme .civicase__activities-calendar th{background:0 0;color:#464354}#bootstrap-theme .civicase__activities-calendar thead th{position:relative;z-index:1}#bootstrap-theme .civicase__activities-calendar thead tr:nth-child(1){background-color:transparent}#bootstrap-theme .civicase__activities-calendar thead .btn{font-size:16px;text-transform:none}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-title{font-weight:600;margin-top:-3px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-left,#bootstrap-theme .civicase__activities-calendar thead .btn.uib-right{margin-top:-3px;max-width:24px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .material-icons{font-size:24px;line-height:24px}#bootstrap-theme .civicase__activities-calendar .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar .uib-title strong{color:#464354;font-size:16px;font-weight:600;line-height:22px}#bootstrap-theme .civicase__activities-calendar .uib-title span:nth-last-child(1){color:#9494a5}#bootstrap-theme .civicase__activities-calendar [uib-daypicker] .uib-title strong{display:none}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] .civicase__activities-calendar__title-word{display:none}#bootstrap-theme .civicase__activities-calendar tr{background-color:#fff;padding:0 5px}#bootstrap-theme .civicase__activities-calendar tbody,#bootstrap-theme .civicase__activities-calendar tr:nth-child(0n+2) th{background:#fff}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n){border-top-left-radius:5px;border-top-right-radius:5px;margin-top:-3px}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) th{color:#9494a5;font-size:10px;padding:21px 0;text-transform:uppercase}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) .current-week-day{color:#0071bd}#bootstrap-theme .civicase__activities-calendar thead th:nth-child(1){border-top-left-radius:2px}#bootstrap-theme .civicase__activities-calendar thead th:nth-last-child(1){border-top-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-child(1){border-bottom-left-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-last-child(1){border-bottom-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tbody{border-bottom-left-radius:5px;border-bottom-right-radius:5px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);min-height:205px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] thead,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] thead{padding-bottom:7px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] tbody,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] tbody{margin-top:-3px;min-height:262px}#bootstrap-theme .civicase__activities-calendar tbody .btn{font-weight:600;padding:10px 0;position:relative;width:100%}#bootstrap-theme .civicase__activities-calendar .btn.active{background-color:#cde1ed;color:#0071bd}#bootstrap-theme .civicase__activities-calendar .uib-day .btn.active{height:28px;margin-top:2px;padding:0;width:28px}#bootstrap-theme .civicase__activities-calendar .uib-day .material-icons{display:none}#bootstrap-theme .civicase__activities-calendar__day-status.uib-day .material-icons{color:#9494a5;display:block;font-size:6px;left:50%;position:absolute;transform:translateX(-50%) translateY(3px);width:6px}#bootstrap-theme .civicase__activities-calendar__day-status--completed.uib-day .material-icons{color:#44cb7e}#bootstrap-theme .civicase__activities-calendar__day-status--overdue.uib-day .material-icons{color:#cf3458}#bootstrap-theme .civicase__activities-calendar__day-status--scheduled.uib-day .material-icons{color:#0071bd}#bootstrap-theme .activities-calendar-popover{border-color:#e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);margin-top:15px;max-width:280px;padding:0}#bootstrap-theme .activities-calendar-popover>.arrow{border-bottom-color:#e8eef0}#bootstrap-theme .activities-calendar-popover .popover-content{max-height:330px;overflow-x:hidden;overflow-y:auto;padding:0}#bootstrap-theme .activities-calendar-popover__footer{border-top:1px solid #e8eef0}#bootstrap-theme .activities-calendar-popover__see-all{padding:10px}#bootstrap-theme .civicase__activities-calendar__dropdown{transform:translateX(calc(-100% + 18px))}#bootstrap-theme .civicase__activity-card--big{display:flex;flex-direction:column;height:auto;min-height:264px;width:100%}#bootstrap-theme .civicase__activity-card--big .panel{flex-grow:1}#bootstrap-theme .civicase__activity-card--big .panel .panel-body{padding:16px 24px 24px}#bootstrap-theme .civicase__activity-card--big .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--big .material-icons{vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-menu{top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-type{flex:1 0 0;font-size:16px;line-height:22px;margin-bottom:12px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date{color:#4d4d69}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date .material-icons{font-size:22px;margin-right:5px;position:relative;top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card .civicase__checkbox{margin-left:2px;margin-right:10px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-additional__container--avatar{margin-left:5px;margin-top:1px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--big .civicase__contact-icon{margin-top:-3px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row{align-items:flex-start}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row.civicase__activity-card-row--first{border-bottom:1px solid #e8eef0;margin:0 -24px 15px;padding:1px 16px 16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container{align-items:center;display:flex;justify-content:center;width:auto}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container span:not(.civicase__activity-icon-ribbon){margin:0 8px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:62px;left:16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:2px;vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__tags-container{margin-bottom:10px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-subject{color:#9494a5;font-size:13px;font-weight:400;line-height:18px;margin:5px 0 17px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-attachment__container,#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{position:relative}#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{margin-left:4px}#bootstrap-theme .civicase__activity-card--big--empty{align-items:center;display:flex;flex-direction:column;justify-content:center;min-height:265px;text-align:center;width:100%}#bootstrap-theme .civicase__activity-card--big--empty.civicase__activity-card--big--empty--list-view{border:1px solid #d3dee2;border-radius:2px;min-height:265px}#bootstrap-theme .civicase__activity-card--big--empty-title{font-size:20px;font-weight:600;line-height:27px;margin:15px 0 5px}#bootstrap-theme .civicase__activity-card--big--empty-description{color:#9494a5;margin-bottom:20px;padding:0 5px}#bootstrap-theme .civicase__activity-card--big--empty-button{border-color:#0071bd!important;font-size:13px;font-weight:600;line-height:18px;padding:10px 16px}#bootstrap-theme .civicase__activity-card--big--empty-button,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:focus{background-color:inherit}#bootstrap-theme .civicase__activity-card--big--empty-button .material-icons{color:#0071bd;margin-right:8px;position:relative;top:-1px}#bootstrap-theme .civicase__activity-card--big--empty-button i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__activity-card--big--empty-button.btn-default:active,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:hover,#bootstrap-theme .civicase__activity-card--big--empty-button:hover .material-icons,#bootstrap-theme .civicase__activity-card--big--empty-button[disabled]:hover{background-color:#0071bd;color:#fff}#bootstrap-theme .civicase__activity-card--long{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);position:relative}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:63px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:12px;top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--first{margin-bottom:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container--ribbon{width:50px}#bootstrap-theme .civicase__activity-card--long .civicase__checkbox{margin-left:10px;margin-right:8px}#bootstrap-theme .civicase__activity-card--long .civicase__tooltip{flex:1;max-width:300px;min-width:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-type{display:block;font-size:16px;margin-right:12px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options{display:inline-block}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options .civicase__activity-card-menu.btn-group>.dropdown-menu{transform:translateX(0)}#bootstrap-theme .civicase__activity-card--long .civicase__tags-container{margin-right:5px;margin-top:-3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-star{position:relative;top:3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date{margin-left:5px;margin-top:-1px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__with-year,#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{vertical-align:middle}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{display:none}#bootstrap-theme .civicase__activity-card--long .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-subject{color:#9494a5;font-weight:400;margin-left:30px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-menu.btn-group .btn{margin-left:0;top:-2px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon{min-height:75px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{min-height:70px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .civicase__activity-subject{margin-left:52px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--with-checkbox .civicase__activity-subject{margin-left:64px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft .panel-footer{border-top:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-arrow{left:22px;top:8px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card__case-type{overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication>div{align-items:center;display:flex}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication .civicase__contact-card{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--short{box-shadow:0 1px 4px 0 rgba(49,40,40,.2);min-height:100px;position:relative;width:280px}#bootstrap-theme .civicase__activity-card--short .panel-body{min-height:100px}#bootstrap-theme .civicase__activity-card--short .civicase__contact-avatar{margin-top:-10px}#bootstrap-theme .civicase__activity-card--short .civicase__activity-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card--short .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--short .civicase__activity-card__case-type{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card{background:#fff;border-radius:5px;cursor:pointer}#bootstrap-theme .civicase__activity-card:hover{background:#f3f6f7}#bootstrap-theme .civicase__activity-card .panel{box-shadow:none;height:100%;margin-bottom:0}#bootstrap-theme .civicase__activity-card .panel-body{background:0 0;border-top:0!important;height:100%;padding:15px}#bootstrap-theme .civicase__activity-card .panel-footer{background:0 0;padding:5px 16px}#bootstrap-theme .civicase__activity-card .civicase__contact-avatar{margin-left:5px}#bootstrap-theme .civicase__activity-card .civicase__checkbox{margin-right:5px;margin-top:2px}#bootstrap-theme .civicase__activity-card .panel-footer:hover{background:#e8eef0}#bootstrap-theme .civicase__activity-card .panel-footer:hover>a:hover{text-decoration:none}#bootstrap-theme .civicase__activity-card-inner{position:relative;width:100%}#bootstrap-theme .civicase__activity-card--empty .panel-body{align-items:center;display:flex;justify-content:center}#bootstrap-theme .civicase__activity-card--draft{border:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--alert{background:#fbf0e2;border:1px solid #e6ab5e}#bootstrap-theme .civicase__activity-card--alert:hover{background:#fbf0e2;border-color:#c2cfd8}#bootstrap-theme .civicase__activity-card--alert .civicase__activity-subject{color:#4d4d69;white-space:initial}#bootstrap-theme .civicase__activity-card--alert .civicase__tags-container{margin-left:30px;margin-top:2px}#bootstrap-theme .civicase__activity-card--file .civicase__activity-subject{color:#464354;font-size:16px;font-weight:600;margin-left:0}#bootstrap-theme .civicase__activity-card__case-id__label,#bootstrap-theme .civicase__activity-card__case-id__value,#bootstrap-theme .civicase__activity-card__case-type{color:#9494a5}#bootstrap-theme .civicase__activity-card__case-id__value{font-weight:600}#bootstrap-theme .civicase__activity-icon-container{color:#0071bd;font-size:18px;width:30px}#bootstrap-theme .civicase__activity-icon-container--ribbon{width:38px}#bootstrap-theme .civicase__activity-icon-container--ribbon .civicase__activity-icon{color:#fff;font-size:16px;left:8px;position:relative;top:-6px;z-index:1}#bootstrap-theme .civicase__activity-icon-arrow{font-size:10px;left:24px;position:absolute;top:11px}#bootstrap-theme .civicase__activity-icon-ribbon{border-bottom:6px solid transparent;border-left:14px solid #0071bd;border-radius:2px;border-right:14px solid #0071bd;height:42px;left:17px;position:absolute;top:-3px;width:0}#bootstrap-theme .civicase__activity-icon-ribbon.text-danger{border-left:14px solid #cf3458;border-right:14px solid #cf3458}#bootstrap-theme .civicase__activity-icon-ribbon.civicase__text-success{border-left:14px solid #44cb7e;border-right:14px solid #44cb7e}#bootstrap-theme .civicase__activity-date{color:#9494a5}#bootstrap-theme .civicase__activity__right-container{margin-left:auto;white-space:nowrap}#bootstrap-theme .civicase__activity__right-container>*{display:inline-block!important;vertical-align:middle}#bootstrap-theme .civicase__activity-type{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-type--completed{text-decoration:line-through}#bootstrap-theme .civicase__activity-subject{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-card-row{align-items:flex-start;display:flex;line-height:1.8em;vertical-align:middle}#bootstrap-theme .civicase__activity-card-row--first{margin-bottom:5px}#bootstrap-theme .civicase__activity-card-row--file{display:block;margin-left:30px}#bootstrap-theme .civicase__activity-card-row--case-info{line-height:2em;white-space:nowrap}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-card{margin-right:5px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__pipe{margin:0 5px}#bootstrap-theme .civicase__activity-with{color:#9494a5}#bootstrap-theme .civicase__activity-star{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-star.active{color:#e6ab5e}#bootstrap-theme .civicase__activity-attachment__container a{display:flex!important}#bootstrap-theme .civicase__activity-attachment__container.open .dropdown-toggle{box-shadow:none}#bootstrap-theme .civicase__activity-attachment__container:hover .dropdown-menu{display:block}#bootstrap-theme .civicase__activity-attachment__dropdown-menu{z-index:1061}#bootstrap-theme .civicase__activity-attachment__file-name{color:#0071bd!important}#bootstrap-theme .civicase__activity-attachment__file-description{color:#9494a5}#bootstrap-theme .civicase__activity-attachment__icon{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-attachment__icon:hover{color:#0071bd}#bootstrap-theme .civicase__activity-card-menu .material-icons{vertical-align:initial}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn{border:0;height:18px;margin-left:6px;padding:0;top:0;width:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn .material-icons{font-size:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn.dropdown-toggle{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card-menu.btn-group>.dropdown-menu{left:50%!important;top:150%!important;transform:translateX(-100%)}#bootstrap-theme .civicase__activity-attachment__file-icon{color:#9494a5}#bootstrap-theme .civicase__activity-attachment-load{padding:10px 20px!important}#bootstrap-theme .civicase__activity-attachment-load-icon{animation:civicase__infinite-rotation 2s linear reverse;font-size:16px;margin-top:3px;position:relative;top:3px}#bootstrap-theme .civicase__activity-empty-message{color:#9494a5;font-size:16px;text-align:center}#bootstrap-theme .civicase__activity-empty-link{display:block;text-align:center}#bootstrap-theme .civicase__activity-no-result-icon{background-position:center center;background-repeat:no-repeat;background-size:contain;height:48px;width:48px}#bootstrap-theme .civicase__activity-no-result-icon--milestone{background-image:url(../resources/icons/milestone.svg)}#bootstrap-theme .civicase__activity-no-result-icon--activity{background-image:url(../resources/icons/activities.svg)}#bootstrap-theme .civicase__activity-no-result-icon--case{background-image:url(../resources/icons/cases.svg)}#bootstrap-theme .civicase__activity-no-result-icon--communications{background-image:url(../resources/icons/comms.svg);width:66px}#bootstrap-theme .civicase__activity-no-result-icon--tasks{background-image:url(../resources/icons/tasks.svg)}#bootstrap-theme .civicase__activity-feed{box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__activity-feed .panel-body{background:0 0;border-top:0!important;padding:15px}#bootstrap-theme .civicase__activity-feed>.panel-body{padding-bottom:0;padding-top:8px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header{margin:auto}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header>.panel-body{background:0 0;border-top:0;box-shadow:none;padding:0}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before{color:#464354!important;padding-left:35px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::before{font-size:14px;height:20px;line-height:20px;top:8px;width:20px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message{margin:0 85px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__activity-feed__activity-container{display:inline-block;margin-left:5px;width:calc(100% - 50px)}#bootstrap-theme .civicase__activity-feed__list{padding-left:10px;padding-right:10px;padding-top:6px;position:relative}#bootstrap-theme .civicase__activity-feed__list.active{background:#b3d5ec;border-radius:5px}#bootstrap-theme .civicase__activity-feed__list.civicase__animated-checkbox-card--expanded{padding-left:50px}#bootstrap-theme .civicase__activity-feed__list__vertical_bar::before{background-color:#c2cfd8;bottom:1px;content:'';left:17px;position:absolute;top:50px;width:8px;z-index:1}#bootstrap-theme .civicase__activity-feed__list-item{display:inline-block;position:relative;width:100%}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card{display:inline-block;margin-top:2px;position:relative;vertical-align:top;z-index:2}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar{height:40px;line-height:30px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image img{height:40px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar__full-name{height:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image:hover .civicase__contact-avatar__full-name{left:39px}#bootstrap-theme .civicase__activity-feed__list-item .civicase__activity-card{margin-bottom:5px;margin-left:auto;margin-right:auto;width:100%}#bootstrap-theme .civicase__checkbox--bulk-action{display:inline-block;margin-right:20px;top:13px;vertical-align:top}#bootstrap-theme .civicase__activity-feed-pager .material-icons{font-size:28px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__more>.btn,#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__no-more>.btn{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__spinner{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager__no-more>.btn{background:0 0;border:0;font-weight:600}#bootstrap-theme .civicase__activity-feed__body{display:flex;justify-content:center;margin:auto;max-width:1330px}#bootstrap-theme .civicase__activity-feed__body__list{flex-grow:1;max-width:630px;min-height:400px;overflow:auto}#bootstrap-theme .civicase__activity-feed__body__details{box-sizing:content-box;min-height:400px;min-width:550px;overflow-y:auto;padding-left:15px;padding-right:10px;padding-top:8px;width:550px}#bootstrap-theme .civicase__activity-feed__body__month-nav{margin-left:15px;min-height:400px;overflow-x:hidden;overflow-y:auto;width:125px}#bootstrap-theme .civicase__activity-feed__placeholder{margin-left:auto;margin-right:auto;width:50%}#bootstrap-theme .civicase__activity-feed__placeholder .civicase__panel-transparent-header{width:100%}@media (max-width:1300px){#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__with-year{display:none}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__without-year{display:inline-block}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{padding:15px 10px 15px 15px}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__tags-container .badge{max-width:35px}#bootstrap-theme .civicase__activity-feed__body__list--details-visible .civicase__contact-name{max-width:40px}}#bootstrap-theme .civicase__activity-filter{background:#e8eef0;padding:16px 40px;width:100%;z-index:11}#bootstrap-theme .civicase__activity-filter__settings .dropdown-toggle{background:0 0!important;box-shadow:none!important;padding:0}#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu{width:250px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li{padding:0 20px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li label,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li label{font-weight:400;margin-left:5px;position:relative;top:3px}#bootstrap-theme .civicase__activity-filter__add .material-icons,#bootstrap-theme .civicase__activity-filter__settings .material-icons{color:#9494a5;font-size:20px;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__add .caret,#bootstrap-theme .civicase__activity-filter__settings .caret{line-height:20px;margin-left:4px;position:relative;top:-5px}#bootstrap-theme .civicase__activity-filter__add,#bootstrap-theme .civicase__activity-filter__others{min-width:150px}#bootstrap-theme .civicase__activity-filter__add .select2-container,#bootstrap-theme .civicase__activity-filter__others .select2-container{height:auto!important;max-width:170px;min-width:170px}#bootstrap-theme .civicase__activity-filter__contact{margin-left:8px}#bootstrap-theme .civicase__activity-filter__contact .btn{border:1px solid #c2cfd8!important}#bootstrap-theme .civicase__activity-filter__contact .btn.active{background:#f3f6f7;box-shadow:inset 0 0 5px 0 rgba(0,0,0,.1);color:#0071bd}#bootstrap-theme .civicase__activity-filter__timeline{width:auto}#bootstrap-theme .civicase__activity-filter__case-type-categories{display:inline-block;margin-left:5px;width:175px}#bootstrap-theme .civicase__activity-filter__category{vertical-align:top;width:175px}#bootstrap-theme .civicase__activity-filter__category .crm-i{color:#4d4d69}#bootstrap-theme .civicase__activity-filter__category .select2-chosen{max-width:130px}#bootstrap-theme .civicase__activity-filter__category,#bootstrap-theme .civicase__activity-filter__timeline{display:inline-block;margin-left:8px}#bootstrap-theme .civicase__activity-filter__category .select2-choice .select2-arrow,#bootstrap-theme .civicase__activity-filter__timeline .select2-choice .select2-arrow{top:0;width:24px}#bootstrap-theme .civicase__activity-filter__attachment,#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{background:0 0!important;padding:5px;text-transform:initial}#bootstrap-theme .civicase__activity-filter__attachment .material-icons,#bootstrap-theme .civicase__activity-filter__more .material-icons,#bootstrap-theme .civicase__activity-filter__star .material-icons{color:#9494a5;font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__activity-filter__attachment.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__more.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#0071bd}#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{padding-left:0}#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#e6ab5e}#bootstrap-theme .civicase__activity-filter__more span{color:#4d4d69;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__more-container{margin-top:15px}#bootstrap-theme .civicase__activity-filter__more-container>*{display:inline-block;margin-bottom:15px;margin-left:5px;margin-right:5px;vertical-align:top}#bootstrap-theme .civicase__activity-filter__custom .civicase__activity-filter__header{border-bottom:1px solid #e8eef0;display:block;margin-top:5px}@media (max-width:1420px){#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter{padding:16px 10px}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__timeline{width:120px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__category{width:165px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more__text{display:none}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__attachment,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__star{padding:5px 2px}}#bootstrap-theme .civicase__activity-month-nav{overflow:hidden;width:105px}#bootstrap-theme .civicase__activity-month-nav.affix{position:fixed!important}#bootstrap-theme .civicase__activity-month-nav__group{border-left:2px solid #d9e1e6}#bootstrap-theme .civicase__activity-month-nav__group-month,#bootstrap-theme .civicase__activity-month-nav__group-title,#bootstrap-theme .civicase__activity-month-nav__group-year{padding-left:15px}#bootstrap-theme .civicase__activity-month-nav__group-title{color:#464354;font-weight:700;text-transform:uppercase}#bootstrap-theme .civicase__activity-month-nav__group-year{color:#464354}#bootstrap-theme .civicase__activity-month-nav__group-gap{height:10px}#bootstrap-theme .civicase__activity-month-nav__group-month{color:#9494a5;cursor:pointer;font-weight:600}#bootstrap-theme .civicase__activity-month-nav__group-month.active{border-left:2px solid #0071bd;color:#0071bd;margin-left:-2px}#bootstrap-theme .civicase__activity-month-nav__group-month:hover{color:#0071bd}#bootstrap-theme .civicase__overdue-activity-icon{color:#cf3458!important;display:inline-block;font-weight:600;padding-right:20px;position:relative}#bootstrap-theme .civicase__overdue-activity-icon::before{background-color:#cf3458;content:''}#bootstrap-theme .civicase__overdue-activity-icon::after{color:#fff;content:'!';top:1px}#bootstrap-theme .civicase__overdue-activity-icon::after,#bootstrap-theme .civicase__overdue-activity-icon::before{border-radius:50%!important;font-size:11px;height:13px;line-height:1em;position:absolute;right:0;text-align:center;top:50%;transform:translateY(-50%);width:13px;z-index:0!important}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before{padding-left:20px;padding-right:0}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::before{left:2px;right:auto}#bootstrap-theme .civicase__activity-panel.affix{position:fixed!important}#bootstrap-theme .civicase__activity-panel .panel{overflow:auto;position:relative}#bootstrap-theme .civicase__activity-panel .panel-heading,#bootstrap-theme .civicase__activity-panel .panel-subheading{position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel .panel-subheading{border-bottom:1px solid #e8eef0;top:63px}#bootstrap-theme .civicase__activity-panel .panel-body{margin-bottom:76px;margin-top:120px;overflow:auto;padding:0}#bootstrap-theme .civicase__activity-panel .panel-subtitle{color:#464354;display:flex;font-size:16px;font-weight:600;line-height:25px}#bootstrap-theme .civicase__activity-panel .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-date{font-size:13px;font-weight:400;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-star{position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-panel__close,#bootstrap-theme .civicase__activity-panel__maximise{color:#464354;font-size:18px;padding:0}#bootstrap-theme .civicase__activity-panel__maximise{margin-right:5px;transform:rotate(45deg)}#bootstrap-theme .civicase__activity-panel__status-dropdown{margin-right:5px}#bootstrap-theme .civicase__activity-panel__status-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__priority-dropdown{margin-right:10px}#bootstrap-theme .civicase__activity-panel__priority-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__id{line-height:33px}#bootstrap-theme .civicase__activity-panel__resume-draft{bottom:20px;height:36px;position:absolute;right:20px}#bootstrap-theme .civicase__activity-panel__core_container{min-height:200px;position:static!important}#bootstrap-theme .civicase__activity-panel__core_container .help{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-activity-form-block-separation{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-block{overflow:auto;padding-top:20px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2)>*,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2)>*{margin-bottom:10px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-radio,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-radio{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel,#bootstrap-theme .civicase__activity-panel__core_container .form-layout{box-shadow:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr{border:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .label{padding-left:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .view-value{padding-right:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label label{color:#9494a5!important;font-weight:400!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .section-shown{padding:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-button_qf_Activity_cancel{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons{border-bottom:0!important;border-top:1px solid #e8eef0;bottom:0;height:auto!important;margin:0;padding:20px!important;position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit{color:#fff!important;background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus{color:#fff!important;background-color:#00538a;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{color:#fff!important;background-color:#00538a;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle{color:#fff!important;background-color:#00538a;background-image:none;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:hover{color:#fff!important;background-color:#003d66;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel .badge,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit .badge{color:#0071bd;background-color:#fff!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete{color:#fff;background-color:#cf3458;border-color:#bd2d4e;background-color:#cf3458!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus{color:#fff;background-color:#a82846;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{color:#fff;background-color:#a82846;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle{color:#fff;background-color:#a82846;background-image:none;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:hover{color:#fff;background-color:#8b213a;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{background-color:#cf3458;border-color:#bd2d4e}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete .badge{color:#cf3458;background-color:#fff}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date-wrapper{display:inline-block}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date{width:153px}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-time{margin-left:10px;width:75px}#bootstrap-theme .civicase__activity-panel__core_container .crm-ajax-select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container input[name=followup_activity_subject]{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value>input,#bootstrap-theme .civicase__activity-panel__core_container .view-value>select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value .crm-form-date-wrapper{margin-bottom:10px}.civicase__badge{border-radius:10px;color:#fff;display:inline-block;font-size:13px;line-height:18px;padding:0 7px}.civicase__badge.text-dark{color:#000}.civicase__badge--default{background:#fff;box-shadow:0 0 0 1px #d3dee2 inset;color:#000}#bootstrap-theme .civicase__bulkactions-checkbox{background:#fff;border:1px solid #c2cfd8;border-radius:2px;display:inline-block;padding:0 3px;position:relative}#bootstrap-theme .civicase__bulkactions-checkbox-toggle{color:#e8eef0;cursor:pointer;font-size:18px;margin-left:3px;transition:.3s color cubic-bezier(0,0,0,.4);vertical-align:middle}#bootstrap-theme .civicase__bulkactions-checkbox-toggle.civicase__checkbox{display:inline-block}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked{color:#0071bd;transition-property:color}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked--hide{color:#c2cfd8;font-size:19.5px;left:3px;top:2px}#bootstrap-theme .civicase__bulkactions-select-mode-dropdown{background:#fff;padding:4px 5px;vertical-align:middle}#bootstrap-theme .civicase__bulkactions-actions-dropdown{margin-left:10px;position:relative}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn{border:1px solid #c2cfd8;line-height:18px;padding:5px 25px 5px 10px;text-transform:unset}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn:hover{border-color:#c2cfd8!important}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn+.dropdown-toggle{padding:5px}#bootstrap-theme .civicase__bulkactions-message .alert{background-color:#f3f6f7;border:1px solid #d3dee2;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);line-height:18px;margin-bottom:0;padding:15px;text-align:center}#bootstrap-theme .civicase__checkbox--bulk-action .civicase__checkbox--checked{color:#0071bd}#bootstrap-theme .civicase__button--with-shadow{box-shadow:0 3px 18px 0 rgba(48,40,40,.25)}#bootstrap-theme .civicase__case-activity-count__popover{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;white-space:nowrap;z-index:11}#bootstrap-theme .civicase__case-activity-count__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-activity-count__popover .arrow.left{left:20px}#bootstrap-theme .civicase__case-body{padding:0}#bootstrap-theme .civicase__case-body .tab-content{background:0 0;position:relative;z-index:0}#bootstrap-theme .civicase__case-body_tab{position:relative;z-index:1}#bootstrap-theme .civicase__case-body_tab.affix{position:fixed;top:0;z-index:11}#bootstrap-theme .civicase__case-body_tab.affix+.tab-content{padding-top:50px}#bootstrap-theme .civicase__case-body_tab>[civicase-dropdown]{opacity:1}#bootstrap-theme .civicase__case-details-panel--summary .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--summary .civicase__case-body_tab.affix{width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--focused .civicase__case-body_tab.affix{width:100%}#bootstrap-theme .civicase__case-card{border-radius:0!important;box-shadow:none;cursor:pointer;height:100%;margin-bottom:0}#bootstrap-theme .civicase__case-card:hover{background:#d9edf7}#bootstrap-theme .civicase__case-card .panel-body{background-color:transparent;border:0!important}#bootstrap-theme .civicase__case-card .civicase__contact-card{font-size:14px;font-weight:600;line-height:18px}#bootstrap-theme .civicase__case-card .civicase__contact-card>span{display:flex}#bootstrap-theme .civicase__case-card .civicase__contact-icon{color:#c2cfd8;font-size:24px;margin-top:-3px}#bootstrap-theme .civicase__case-card .civicase__checkbox{left:20px;top:15px}#bootstrap-theme .civicase__case-card .civicase__tags-container .badge{max-width:195px}#bootstrap-theme .civicase__case-card--closed{background-image:repeating-linear-gradient(60deg,#e8eef0,#e8eef0 2px,#f3f6f7 2px,#f3f6f7 20px);min-height:149px}#bootstrap-theme .civicase__case-card--closed .civicase__case-card-subject,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__type,#bootstrap-theme .civicase__case-card--closed .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--closed .civicase__contact-name{text-decoration:line-through}#bootstrap-theme .civicase__case-card--closed .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__next-milestone-date{color:inherit;font-weight:400}#bootstrap-theme .civicase__case-card--case-list .civicase__case-card__activity-info{overflow:hidden;white-space:nowrap}#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name,#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name-additional{max-width:100px}#bootstrap-theme .civicase__case-card--other{border-bottom:1px solid #e8eef0;min-height:auto}#bootstrap-theme .civicase__case-card--other .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--other .civicase__contact-name{color:#464354;font-size:16px}#bootstrap-theme .civicase__case-card--other .civicase__contact-name{max-width:none}#bootstrap-theme .civicase__case-card--other .civicase__case-card__activity-info{display:inline-flex}#bootstrap-theme .civicase__case-card--other .civicase__case-card__next-milestone{margin-right:30px}#bootstrap-theme .civicase__case-card__right_container{color:#9494a5}#bootstrap-theme .civicase__case-card__dates{margin-right:16px;vertical-align:text-top}#bootstrap-theme .civicase__case-card__link-type{font-size:16px;margin-left:16px;vertical-align:middle}#bootstrap-theme .civicase__case-card--active{border-bottom:1px solid #0071bd!important;border-right:1px solid #0071bd!important;border-top:1px solid #0071bd!important}#bootstrap-theme .civicase__case-card__additional-information{line-height:normal;position:absolute;right:20px;top:15px}#bootstrap-theme .civicase__case-card__case-id{color:#9494a5}#bootstrap-theme .civicase__case-card__lock{color:#c2cfd8;font-size:24px;line-height:0;position:relative;top:5px}#bootstrap-theme .civicase__case-card__type{color:#4d4d69}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__contact,#bootstrap-theme .civicase__case-card__next-milestone,#bootstrap-theme .civicase__case-card__type{margin-bottom:3px}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__next-milestone{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card__next-milestone-date{color:#0071bd;font-weight:600}#bootstrap-theme .civicase__case-card__activity-count:hover,#bootstrap-theme .civicase__case-card__next-milestone-date:hover{text-decoration:none}#bootstrap-theme .civicase__case-card__activity-count--zero{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count-container{display:inline-block;margin-right:8px}#bootstrap-theme .civicase__case-card--detached{background:#fff;border-radius:2px!important;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);color:#9494a5;margin-bottom:10px}#bootstrap-theme .civicase__case-card--detached>.panel-body{padding:0}#bootstrap-theme .civicase__case-card--detached .civicase__case-card__date{color:#4d4d69}#bootstrap-theme .civicase__case-card--detached .civicase__contact-icon{font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__case-card--detached .crm_notification-badge{vertical-align:unset}#bootstrap-theme .civicase__case-card--detached .civicase__case-card-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed{background:#f3f6f7;min-height:auto}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-role,#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-subject{color:inherit}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--active{box-shadow:0 0 0 5px #b3d5ec}#bootstrap-theme .civicase__case-card-role-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-card-role-container .material-icons{margin-right:5px}#bootstrap-theme .civicase__case-card-role,#bootstrap-theme .civicase__case-card-subject{color:#4d4d69;line-height:18px}#bootstrap-theme .civicase__case-card__row{border-bottom:1px solid #e8eef0;clear:both}#bootstrap-theme .civicase__case-card__row:last-child{border-bottom:0}#bootstrap-theme .civicase__case-card__row--primary{padding:15px}#bootstrap-theme .civicase__case-card__row--secondary{padding:10px 15px}#bootstrap-theme .civicase__case-custom-fields__container.civicase__summary-tab-tile{padding-top:0}#bootstrap-theme .civicase__case-custom-fields__container civicase-masonry-grid-item:not(:first-child){margin-top:30px}#bootstrap-theme .civicase__case-custom-fields__container .panel-body{border-top:0!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-editable-enabled:not(.crm-editable-editing):hover{border:2px dashed transparent;padding:24px}#bootstrap-theme .civicase__case-custom-fields__container .crm-case-custom-form-block table{width:100%}#bootstrap-theme .civicase__case-custom-fields__container .crm-submit-buttons{border:0;padding:15px 8px 0;text-align:right}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit{min-width:auto;padding:7px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel{padding:6px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel:hover{color:#fff!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container{background:#fff;padding:20px}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container .crm-block{box-shadow:none}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row label{color:#9494a5;font-weight:400!important}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row:not(:first-child){display:block;margin-top:16px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:first-child{display:block;text-align:left}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2){display:block;margin-left:7px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .cke,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) textarea{width:calc(100% - 4px)}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .select2-arrow{padding-right:20px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-radio{margin-right:8px;margin-top:-4px}#bootstrap-theme .civicase__case-details__add-new-dropdown{left:-20px;position:relative;top:6px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary{border:1px solid #fff;line-height:20px;padding:7px 16px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-child(1){margin-right:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown]{position:relative}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu{left:auto;right:0;width:180px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown] .dropdown-menu{top:0}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu .fa-fw,#bootstrap-theme .civicase__case-details__add-new-dropdown a .material-icons{color:#0071bd}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu{max-height:260px;overflow-x:hidden;overflow-y:scroll;padding:8px 0 0;right:170px;top:0;width:220px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control{margin:0 auto;width:204px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control-feedback{color:#464354;font-size:14px;margin-right:8px;margin-top:7px;position:absolute}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu a{padding:9px 17px;white-space:normal}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters a{padding:9px 17px 9px 38px}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters .fa-fw{margin-left:-21px;margin-right:4px}#bootstrap-theme [civicase-dropdown]{position:relative}#bootstrap-theme [civicase-dropdown] .dropdown-menu{top:calc(100% + 8px)}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list{margin-left:auto;margin-right:auto;width:685px}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list::before{content:none}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message{margin:0 85px 10px}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__file-tab-filters{display:inline-block;float:right}#bootstrap-theme .civicase__file-filters-container{display:flex}#bootstrap-theme .civicase__file-filters{margin-left:16px;width:180px}#bootstrap-theme .civicase__file-filters .select2-container{height:auto!important}#bootstrap-theme .civicase__file-filters:not(:nth-child(2)) .form-control:not(.select2-container){width:180px}#bootstrap-theme .civicase__file-filters .input-group-addon{font-size:14px;padding:0;width:30px!important}#bootstrap-theme .civicase__file-filters .input-group-addon .material-icons{position:relative;top:2px}#bootstrap-theme .civicase__case-header{background:#fff;position:relative}#bootstrap-theme .civicase__case-header__expand_button{background:0 0;border-left:1px solid #e8eef0;border-right:1px solid #e8eef0;bottom:0;color:#c2cfd8;font-size:30px;left:0;padding:0;position:absolute;top:0;width:56px}#bootstrap-theme .civicase__case-header__expand_button>.material-icons{vertical-align:middle}#bootstrap-theme .civicase__case-header__content{border-top:1px solid #d3dee2;padding:15px 15px 15px 80px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client{color:#464354;font-size:24px;font-weight:600}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-icon{font-size:30px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-additional__arrow{top:-18px}#bootstrap-theme .civicase__case-header__content .civicase__contact-name{margin-top:1px;max-width:300px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager{display:inline-block;position:relative;top:4px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__case-header__case-type+.civicase__pipe{margin-right:5px}#bootstrap-theme .civicase__case-header__content .civicase__tags-container{position:relative;top:-1px}#bootstrap-theme .civicase__case-header__webform-dropdown+.dropdown-menu>li a{max-width:500px!important}#bootstrap-theme .civicase__case-header__content__first-row{display:flex;min-height:42px}#bootstrap-theme .civicase__case-header__content__trash{font-size:30px;margin-right:5px;position:relative;top:2px;width:20px}#bootstrap-theme .civicase__case-header__case-info,#bootstrap-theme .civicase__case-header__dates{color:#9494a5}#bootstrap-theme .civicase__case-header__case-info{margin-top:5px}#bootstrap-theme .civicase__case-header__case-id,#bootstrap-theme .civicase__case-header__case-source,#bootstrap-theme .civicase__case-header__case-type{color:#464354}#bootstrap-theme .civicase__case-header__case-type a{display:inline}#bootstrap-theme .civicase__case-header__action-menu{position:absolute;right:20px;top:20px}#bootstrap-theme .civicase__case-header__action-menu .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li a{max-width:200px;overflow:hidden;position:relative;text-overflow:ellipsis}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu{left:auto;margin-top:-40px;position:absolute;right:100%}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu a{max-width:500px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li:hover>.dropdown-menu.sub-menu{display:block;opacity:1;visibility:visible}#bootstrap-theme .civicase__case-header__action-icon{font-size:20px;padding:2px 10px}#bootstrap-theme .civicase__case-header__action-icon .material-icons{position:relative;top:3px}#bootstrap-theme .civicase__case-tab--linked-cases .civicase__summary-tab__other-cases{margin-left:0;margin-right:0}#bootstrap-theme .civicase__panel-empty{margin-bottom:110px;margin-top:110px;padding:5px;text-align:center}#bootstrap-theme .civicase__panel-empty .fa.fa-big,#bootstrap-theme .civicase__panel-empty .material-icons{color:#9494a5;font-size:64px}#bootstrap-theme .civicase__panel-empty .empty-label{color:#9494a5;font-size:14px;font-weight:600;line-height:19px;padding:18px 0;text-align:center}#bootstrap-theme .civicase__case-list-table-container{border-left:1px solid #e8eef0;margin-left:300px;overflow-x:auto;overflow-y:visible}#bootstrap-theme .civicase__case-list-table{table-layout:inherit}#bootstrap-theme .civicase__case-list-table td:first-child,#bootstrap-theme .civicase__case-list-table th:first-child{width:300px}#bootstrap-theme .civicase__case-list-table th{line-height:18px;min-width:142px;padding:22px 15px!important}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-checkbox{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown .civicase__bulkactions-actions-dropdown__text{width:60px}#bootstrap-theme .civicase__case-list-table .civicase__case-list-column--first{padding:16px 14px!important}#bootstrap-theme .civicase__case-list-table th:first-child{background:#f3f6f7;left:0;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-table th:nth-child(2){max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table tr{height:150px}#bootstrap-theme .civicase__case-list-table thead tr{height:63px}#bootstrap-theme .civicase__case-list-table td{height:150px;min-width:142px;padding:20px}#bootstrap-theme .civicase__case-list-table td:first-child{background:#fff;left:0;padding:0;position:absolute;width:300px;z-index:1}#bootstrap-theme .civicase__case-list-table td:nth-child(2){vertical-align:middle}#bootstrap-theme .civicase__case-list-table .case-activity-card-wrapper{max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table__column--status_badge{max-width:200px;min-width:200px!important}#bootstrap-theme .civicase__case-list-table__column--status_badge .crm_notification-badge{display:block;max-width:fit-content;overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__case-list-table__header.affix{display:block;left:0;margin-left:300px;overflow-x:hidden;overflow-y:visible;right:0;top:60px;z-index:10}#bootstrap-theme .civicase__case-list-table__header.affix tr{display:table;width:100%}#bootstrap-theme .civicase__case-list-table__header.affix th{border-bottom:1px solid #e8eef0;display:table-cell}#bootstrap-theme .civicase__case-list-table__header.affix th:nth-child(1){left:0;position:fixed}#bootstrap-theme .civicase__case-list{margin:0;overflow:hidden;position:relative}#bootstrap-theme .civicase__case-list .civicase__bulkactions-message .alert{border-bottom:0}#bootstrap-theme .civicase__case-list .civicase__pager--fixed{position:fixed}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case{width:300px}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case.civicase__pager--fixed{position:absolute}#bootstrap-theme .civicase__case-list .civicase__pager--case-focused{display:none}#bootstrap-theme .civicase__case-list--summary>.civicase__pager{bottom:0;height:60px;position:absolute}#bootstrap-theme .civicase__case-list--summary .civicase__case-list-table-container{overflow-x:hidden}#bootstrap-theme .civicase__case-list-panel{border-top:1px solid #d3dee2;box-shadow:none;margin-bottom:0;overflow:auto;padding:0;position:relative;transition:width .3s linear}#bootstrap-theme .civicase__case-list-panel--summary{border-top:0;bottom:60px;overflow-x:hidden;position:absolute;top:65px;width:300px}#bootstrap-theme .civicase__case-list-panel--summary thead{display:none}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-column--first{background:#fff!important}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-table td:first-child{width:100%}#bootstrap-theme .civicase__case-list-column--first--detached{background:#fff;border-bottom:1px solid #d3dee2;border-top:1px solid #d3dee2;height:65px;left:0;padding:16px 14px;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-panel--focused{width:0}#bootstrap-theme .civicase__case-list-panel--focused .civicase__pager{display:none}#bootstrap-theme .civicase__case-details-panel{box-shadow:none;display:none;float:right;height:0;overflow:hidden;transition:width .3s;width:0}#bootstrap-theme .civicase__case-details-panel>.panel-body{background:0 0}#bootstrap-theme .civicase__case-details-panel .civicase__panel-empty{background:#fff;height:100%;margin:0;padding:15px 20px}#bootstrap-theme .civicase__case-details-panel--summary{display:block;height:100%;overflow-y:auto;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused{width:100%}#bootstrap-theme .civicase__case-list-sortable-header{cursor:pointer}#bootstrap-theme .civicase__case-list-sortable-header:hover{background-color:#e8eef0}#bootstrap-theme .civicase__case-list-sortable-header.active{background-color:#e8eef0!important}#bootstrap-theme .civicase__case-list__toggle-sort{color:#9494a5;cursor:pointer;font-size:20px;position:relative;top:7px}#bootstrap-theme .civicase__case-list__header-toggle-sort{float:right;position:relative;top:3px}#bootstrap-theme .civicase__case-sort-dropdown{box-shadow:none;display:inline-block;width:90px!important}#bootstrap-theme .civicase__case-overview .panel-body{background-color:#fafafb;padding:0!important}#bootstrap-theme .civicase__case-overview paging{padding-right:20px}#bootstrap-theme .civicase__case-overview-container a{color:inherit;text-decoration:none}#bootstrap-theme .civicase__case-overview-container .civicase__case-overview__flow,#bootstrap-theme .civicase__case-overview-container .simplebar-content{position:static}#bootstrap-theme .civicase__case-overview-container .simplebar-content{padding-right:0!important}#bootstrap-theme .civicase__case-overview__breakdown,#bootstrap-theme .civicase__case-overview__flow{display:flex;margin-left:200px}#bootstrap-theme .civicase__case-overview__breakdown-field,#bootstrap-theme .civicase__case-overview__flow-status{display:inline-flex;flex-basis:200px;flex-grow:1;flex-shrink:0}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child,#bootstrap-theme .civicase__case-overview__flow-status:first-child{align-items:flex-start;align-items:center;flex-direction:row;justify-content:flex-start;left:0;position:absolute;top:auto;width:200px;z-index:5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child .civicase__case-overview__flow-status__icon,#bootstrap-theme .civicase__case-overview__flow-status:first-child .civicase__case-overview__flow-status__icon{color:#0071bd;cursor:pointer;margin-left:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-overview__flow-status{align-items:flex-start;background-color:#fff;flex-direction:column;height:80px;justify-content:center;position:relative}#bootstrap-theme .civicase__case-overview__flow-status::after{background-color:#fff;border-bottom-right-radius:5px;box-shadow:1px 1px 0 0 #e8eef0;content:'';height:57px;position:absolute;right:-25px;top:12px;transform:rotateZ(-45deg);transform-origin:50% 50%;width:57px;z-index:1}#bootstrap-theme .civicase__case-overview__flow-status:first-child{font-size:16px;font-weight:600;line-height:22px;padding-left:24px;z-index:10}#bootstrap-theme .civicase__case-overview__flow-status:last-child{overflow:hidden}#bootstrap-theme .civicase__case-overview__flow-status:last-child::after{content:none}#bootstrap-theme .civicase__case-overview__flow-status-settings{position:relative}#bootstrap-theme .civicase__case-overview__flow-status-settings .btn{color:inherit;margin-right:10px;padding:3px 0;text-decoration:none!important}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings{color:inherit!important;margin:0!important;vertical-align:middle}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings.material-icons{color:#9494a5!important;font-size:18px}#bootstrap-theme .civicase__case-overview__flow-status__border{bottom:0;height:4px;position:absolute;transform:skewx(-45deg);width:calc(100% - 1px);z-index:2}#bootstrap-theme .civicase__case-overview__flow-status__count{color:#464354;font-size:24px;font-weight:600;line-height:33px;text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__empty-state{text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__description{color:#9494a5;margin:0 auto;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:140px}#bootstrap-theme .civicase__case-overview__flow-status__description span{text-overflow:ellipsis}#bootstrap-theme .civicase__case-overview__breakdown:last-child .civicase__case-overview__breakdown-field{border:0}#bootstrap-theme .civicase__case-overview__breakdown-field{align-items:center;border-bottom:1px solid #e8eef0;justify-content:center;padding:16px 24px;position:relative}#bootstrap-theme .civicase__case-overview__breakdown-field:not(:first-child){color:#9494a5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after,#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{background-color:#fafafb;bottom:-1px;content:'';height:1px;position:absolute;width:24px}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child{background-color:#fafafb;font-weight:600}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child a{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after{left:0}#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{right:0}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover::after,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover::after{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:0;z-index:1}#bootstrap-theme .civicase__case-overview__popup.dropdown-menu{margin-top:15px;padding:8px 0}#bootstrap-theme .civicase__case-overview__popup .popover-content{padding:0}#bootstrap-theme .civicase__case-overview__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-overview__popup .arrow.left{left:20px}#bootstrap-theme .civicase__case-overview__popup .dropdown-menu{box-shadow:none;display:inherit;position:inherit}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox{display:inline-block;margin-right:5px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox--checked{color:#0071bd!important;font-size:24px!important;top:0!important}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container{line-height:18px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-tab--people .nav-tabs{border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03)}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox{margin-right:16px}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox .civicase__people-tab__table-checkbox{cursor:pointer;height:100%;left:0;opacity:0;position:absolute;right:0;top:0;width:100%;z-index:11}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab-link{line-height:18px;padding-bottom:15px;padding-top:12px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-header .civicase__people-tab__table-column{line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-body .civicase__people-tab__table-column{padding:16px 20px}#bootstrap-theme .civicase__people-tab{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__people-tab__sub-tab .civicase__add-btn{margin-right:-6px;margin-top:-4px}#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled],#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled]:hover{color:#9494a5;cursor:not-allowed}#bootstrap-theme .civicase__people-tab__search{background:#fff;padding:20px 30px}#bootstrap-theme .civicase__people-tab__search h3{margin:0}#bootstrap-theme .civicase__people-tab__search .btn .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__people-tab__search .dropdown-menu{left:auto!important;right:0;top:100%!important}#bootstrap-theme .civicase__people-tab__search .civicase__bulkactions-actions-dropdown .dropdown-menu{right:auto}#bootstrap-theme .civicase__people-tab__selection{align-items:center;display:flex;padding:16px 0}#bootstrap-theme .civicase__people-tab__selection>input{margin:0 5px 0 9px}#bootstrap-theme .civicase__people-tab__selection label{line-height:18px;margin-bottom:0;position:relative;top:1px}#bootstrap-theme .civicase__people-tab__select-box .form-control{width:240px}#bootstrap-theme .civicase__people-tab__filter{align-items:center;border-bottom:1px solid #e8eef0;border-top:1px solid #e8eef0;display:flex;justify-content:space-between;padding:10px 24px}#bootstrap-theme .civicase__people-tab__filter--role .form-control{width:160px}#bootstrap-theme .civicase__features-filters{display: flex;justify-content: space-between;}#bootstrap-theme .civicase__people-tab__filter--relations{justify-content:unset}#bootstrap-theme .civicase__people-tab__filter-alpha-pager{margin-left:20px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link{color:#464354;margin:0 3px;padding:2px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.active,#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.all{color:#0071bd;text-decoration:none}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link:first-child{margin-left:0;padding-left:0}#bootstrap-theme .civicase__people-tab__table-column--first{display:flex}#bootstrap-theme .civicase__people-tab__table-column--first em{font-weight:400}#bootstrap-theme .civicase__people-tab__table-column--first input{margin:0}#bootstrap-theme .civicase__people-tab__table-column--last{padding:20px 0!important}#bootstrap-theme .civicase__people-tab__table-column--last .dropdown-menu{left:auto!important;right:20px;top:100%!important}#bootstrap-theme .civicase__people-tab__table-column--last .btn{padding:0}#bootstrap-theme .civicase__people-tab__table-column--last .open{position:relative}#bootstrap-theme .civicase__people-tab__table-column--last .open .dropdown-toggle{background:0 0!important;box-shadow:none}#bootstrap-theme .civicase__people-tab__table-column--last .material-icons{font-size:18px;padding:0}#bootstrap-theme .civicase__people-tab__inactive-filter{margin-left:auto;margin-right:30px}#bootstrap-theme .civicase__people-tab__inactive-filter .civicase__checkbox{display:inline-block;margin-right:5px;top:5px}#bootstrap-theme .civicase__people-tab__table-assign-icon{cursor:pointer}#bootstrap-theme .civicase__people-tab__table-assign-icon:hover{color:#0071bd}#bootstrap-theme .civicase__people-tab-counter{border-top:1px solid #e8eef0;line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-filter-panel{background-color:#f3f6f7;box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__case-filter-panel .panel-header{position:relative}#bootstrap-theme .civicase__case-filter-panel__title{font-size:18px;left:20px;line-height:24px;margin:0;max-width:calc(((100% - 950px)/ 2) - 20px);overflow:hidden;position:absolute;text-overflow:ellipsis;top:50%;transform:translateY(-50%);white-space:nowrap}#bootstrap-theme .civicase__case-filters-container{display:flex;justify-content:center;left:0;padding:13.5px 0;top:0}#bootstrap-theme .civicase__case-filter__input.form-control{margin:0 8px}#bootstrap-theme .civicase__case-filter__input.form-control:not(.select2-container){width:240px}#bootstrap-theme .civicase__case-filter-panel__button{margin:0 8px;width:158px}#bootstrap-theme .civicase__case-filter-panel__button:first-child{margin-left:0}#bootstrap-theme .civicase__case-filter-panel__button .fa{font-size:18px;margin-right:7px;position:relative;top:2px}#bootstrap-theme .civicase__case-filter-form-elements-container{margin:0 auto;width:926px}#bootstrap-theme .civicase__case-filter-form-elements{clear:both;margin-bottom:10px}#bootstrap-theme .civicase__case-filter-form-elements .select2-choices{padding-right:30px}#bootstrap-theme .civicase__case-filter-form-elements .form-control{max-width:385px}#bootstrap-theme .civicase__case-filter-form-elements.civicase__case-filter-form-elements--case-id .form-control{max-width:160px}#bootstrap-theme .civicase__case-filter-form-elements label,#bootstrap-theme .civicase__case-filter-form-elements-container .civicase__checkbox__container label{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__case-filter-panel__description{align-items:center;display:flex;flex-direction:row}#bootstrap-theme .civicase__filter-search-description-list-container{flex:1 0 0;margin-bottom:0}#bootstrap-theme .civicase__case-filter-form-legend{border-color:#e8eef0;color:#464354;font-size:16px;font-weight:600;line-height:22px;margin-bottom:20px;padding:0 0 8px}#bootstrap-theme .civicase__case-filter-fieldset{margin:15px 0}#bootstrap-theme .civicase__case-summary-fields:not(:first-child){margin-top:16px}#bootstrap-theme .civicase__case-summary-fields__label{color:#9494a5}#bootstrap-theme .civicase__case-summary-fields__value{color:#464354;word-break:break-all}#bootstrap-theme .civicase__case-tab__container{padding:24px 30px}#bootstrap-theme .civicase__case-tab__actions{margin-bottom:16px}#bootstrap-theme .civicase__case-tab__empty{color:#464354;font-weight:600;margin-top:40px;opacity:.65}#bootstrap-theme .civicase__checkbox{background-color:#fff;border:1px solid #c2cfd8;border-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03);box-sizing:border-box;cursor:pointer;display:inline-block;height:18px;margin-right:5px;position:relative;width:18px}#bootstrap-theme .civicase__checkbox__container .control-label{position:relative;top:-4px}#bootstrap-theme .civicase__checkbox--checked{color:#c2cfd8;font-size:24px;left:0;margin-left:-4px;margin-top:-4px;position:absolute;top:0;transition:.2s all cubic-bezier(0,0,0,.4);z-index:10}#bootstrap-theme .civicase__animated-checkbox-card{position:relative;transition:.1s padding-left cubic-bezier(0,0,0,.4);transition-delay:.1s}#bootstrap-theme .civicase__animated-checkbox-card .civicase__checkbox--bulk-action{cursor:pointer;left:14px;opacity:0;outline:0;position:absolute;top:15px;transform:scale(.3);transition:.1s all cubic-bezier(0,0,0,.4);transition-delay:unset}#bootstrap-theme .civicase__animated-checkbox-card--expanded{padding-left:30px;transition-delay:0}#bootstrap-theme .civicase__animated-checkbox-card--expanded .civicase__checkbox--bulk-action{opacity:1;transform:scale(1);transition-delay:.1s}.civicase__contact-activity-tab__add .select2-container .select2-choice{background:#4d4d69;border:0;box-shadow:none;height:auto;line-height:initial;padding:7px 19px;width:155px!important}.civicase__contact-activity-tab__add .select2-container .select2-chosen{color:#fff!important;margin:0;text-transform:uppercase}.civicase__contact-activity-tab__add .select2-container .select2-arrow{background:0 0!important;border:0;line-height:34px}.civicase__contact-activity-tab__add .select2-container .select2-arrow::before{color:#fff!important}#bootstrap-theme .civicase__contact-card{color:#0071bd;display:flex}#bootstrap-theme .civicase__contact-name-container{display:flex}#bootstrap-theme .civicase__contact-name,#bootstrap-theme .civicase__contact-name-additional{color:inherit;margin-left:5px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__contact-icon,#bootstrap-theme .civicase__contact-icon-additional{color:#c2cfd8;font-size:18px;margin-top:2px}#bootstrap-theme .civicase__contact-icon-additional.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon-additional:hover,#bootstrap-theme .civicase__contact-icon.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon:hover{color:#0071bd;cursor:pointer}#bootstrap-theme .civicase__contact-icon .material-icons,#bootstrap-theme .civicase__contact-icon-additional .material-icons{line-height:inherit}#bootstrap-theme .civicase__contact-additional__container{margin-left:8px}#bootstrap-theme .civicase__contact-additional__container,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts{color:#0071bd;padding-left:0!important;padding-right:0!important}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-icon,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-icon{font-size:18px}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-name-additional,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-name-additional{color:#0071bd}#bootstrap-theme .civicase__contact-additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer}#bootstrap-theme .civicase__contact-additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__contact-additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__contact-additional__popover .popover-content{padding:0}#bootstrap-theme .civicase__contact-additional__list{margin:0;padding:0}#bootstrap-theme .civicase__contact-additional__list li{height:34px;list-style:none;padding:7px 20px}#bootstrap-theme .civicase__contact-additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__contact-additional__list li .civicase__contact-icon{vertical-align:middle}#bootstrap-theme .civicase__contact-additional__list li a{text-decoration:none}#bootstrap-theme .civicase__contact-additional__hidden_contacts_info{color:#9494a5;font-size:12px}#bootstrap-theme .civicase__contact-avatar{background:#99c6e5;min-width:30px;padding:5px;position:relative}#bootstrap-theme .civicase__contact-additional__container--avatar{background:#99c6e5;margin-left:0;margin-top:-10px;min-width:30px;padding:5px}#bootstrap-theme .civicase__contact-avatar--image{background:0 0;padding:0;position:relative;z-index:1}#bootstrap-theme .civicase__contact-avatar--image img{border-radius:2px;height:25px;width:25px}#bootstrap-theme .civicase__contact-avatar__full-name{background:#99c6e5;border-radius:1px;display:none;height:30px;left:0;padding:5px 10px;position:absolute;top:0;width:auto}#bootstrap-theme .civicase__contact-avatar--image .civicase__contact-avatar__full-name{border-bottom-left-radius:0;border-top-left-radius:0}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover{opacity:1}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover .civicase__contact-avatar__full-name{display:block}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover.civicase__contact-avatar--image .civicase__contact-avatar__full-name{left:29px;padding-left:11px;z-index:0}#bootstrap-theme .civicase__contact-card__with-more-fields{flex-wrap:wrap}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__more-field{color:#464354}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__break{flex-basis:100%;height:0}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact-name{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields{max-height:300px;overflow-y:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li{height:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li:not(:first-of-type){border-top:1px solid #e8eef0}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact-name-additional{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact__more-field{margin-top:5px}#bootstrap-theme .civicase__contact-cases-tab{margin-left:-5px}#bootstrap-theme .civicase__contact-cases-tab-container{padding-left:15px;padding-right:15px}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:18px;margin:0;padding:15px 0}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-body{background:0 0;box-shadow:none;padding:0}#bootstrap-theme .civicase__contact-case-tab__case-list__footer{margin-top:24px}#bootstrap-theme .civicase__contact-case-tab__case-list__footer .btn{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__contact-cases-tab-empty{align-items:center;display:flex;flex-direction:column;padding:50px 0}#bootstrap-theme .civicase__contact-cases-tab-empty a{color:#4d4d69}#bootstrap-theme .civicase__contact-cases-tab-add{background:#4d4d69;margin-bottom:10px}#bootstrap-theme .civicase__contact-cases-tab-add .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);margin-top:calc((1.1 * 18px) + 2 * 15px)}#bootstrap-theme .civicase__contact-cases-tab-details>.panel-body{padding:0}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group{margin-right:5px}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group:last-child{margin-right:0}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__tags-container{max-width:80%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__activity-card{width:100%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__subject{margin-bottom:3px;margin-left:-2px;margin-top:20px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__description{margin-bottom:5px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client{position:relative;top:8px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client .material-icons,#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager{position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__status-label{display:inline-block;max-width:250px;overflow:hidden;position:relative;text-overflow:ellipsis;top:3px;white-space:nowrap}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__case-link .fa{font-size:17px;margin-right:5px;position:relative;top:1px}#bootstrap-theme .civicase__contact-cases-tab-details .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__contact-cases-tab-details__title{margin:6.5px 0}#bootstrap-theme .civicase__contact-cases-tab__panel-row{border-bottom:1px solid #e8eef0;padding:15px 24px}#bootstrap-theme .civicase__contact-cases-tab__panel-row:last-child{border-bottom:0}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__subject textarea{min-height:65px}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__description textarea{min-height:85px}#bootstrap-theme .civicase__contact-cases-tab__panel-actions{padding:20px}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark{background-color:#f3f6f7}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark .civicase__pipe{color:#e8eef0;margin:0 8px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields{padding-bottom:15px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields--inline{align-items:center;display:flex}#bootstrap-theme .civicase__contact-cases-tab__panel-field-emphasis{color:#9494a5}#bootstrap-theme .civicase__contact-cases-tab__panel-field-title{color:#9494a5;margin-bottom:5px}.crm-contact-page #ui-id-5{padding:30px;width:calc(100% - 200px)}@media (max-width:1400px){#bootstrap-theme .civicase__contact-card--client{clear:both}}.contact-popover-container{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;max-width:90vw;padding:20px;width:708px}.contact-popover-container .popover-content{padding:0}.contact-popover-container.bottom>.arrow{border-bottom-color:#e8eef0}.contact-popover-container.top>.arrow{border-top-color:#e8eef0}.civicase__contact-popover__header h2{line-height:24px;margin:0}.civicase__contact-popover__header hr{background-color:#e8eef0;margin:16px 0}.civicase__contact-popover__column{float:left;width:46%}.civicase__contact-popover__column+.civicase__contact-popover__column{width:54%}.civicase__contact-popover__detail-group{float:left;margin-bottom:10px;width:100%}.civicase__contact-popover__detail-header,.civicase__contact-popover__detail-value{color:#4d4d69;float:left;line-height:18px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;width:55%}.civicase__contact-popover__detail-header strong,.civicase__contact-popover__detail-value strong{color:#464354;font-weight:600}.civicase__contact-popover__detail-header{clear:both;width:45%}#bootstrap-theme .civicrm__contact-prompt-dialog textarea{width:100%}#bootstrap-theme .civicrm__contact-prompt-dialog__date-error.crm-error{background:#fbe3e4}#bootstrap-theme .civicase__crm-dashboard__tabs{position:relative;width:100%;z-index:1}#bootstrap-theme .civicase__crm-dashboard__tabs.affix{position:fixed;z-index:11}#bootstrap-theme .civicase__crm-dashboard__myactivities-tab{padding:0}#bootstrap-theme .civicase__dashboard__tab{background:#e8eef0}#bootstrap-theme .civicase__dashboard__tab>.civicase__dashboard__tab__top{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab .panel-secondary{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__col{padding:0 15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{display:flex;flex-direction:column;margin:0 -15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__main{margin:auto;max-width:1081px;padding:0 30px}@media (min-width:992px){#bootstrap-theme .civicase__dashboard__tab__col--left{flex-basis:330px;flex-grow:0;flex-shrink:0}#bootstrap-theme .civicase__dashboard__tab__col--right{flex-grow:1}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{flex-direction:row}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:0}}@media (min-width:1200px){#bootstrap-theme .civicase__dashboard__tab__main{padding:0}}#bootstrap-theme .civicase__dashboard .tab-pane{padding:0}#bootstrap-theme .civicase__dashboard__tab-container .nav{width:100%;z-index:10}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix-top{position:relative}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix{position:fixed}#bootstrap-theme .civicase__dashboard-activites-feed{background:#e8eef0}#bootstrap-theme .civicase__dashboard__action-btn{background:#0071bd;border:1px solid #fff;border-radius:2px;color:#fff;line-height:20px;margin-right:15px;margin-top:7px;padding:6px 16px}#bootstrap-theme .civicase__dashboard__action-btn .material-icons{font-size:16px;margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__dashboard__action-btn--light{background:#fff;color:#0071bd}#bootstrap-theme .civicase__dashboard__relation-filter{display:inline-block;margin-right:10px;margin-top:7px;width:190px}#bootstrap-theme .civicase__dashboard__relation-filter .select2-choice{color:#9494a5}#bootstrap-theme .civicase__ui-range>span{display:inline-block;margin-right:16px}#bootstrap-theme .civicase__ui-range .crm-form-date{display:inline-block;width:134px}#bootstrap-theme .civicase__ui-range .crm-form-date-wrapper{display:inline-block;position:relative}#bootstrap-theme .civicase__ui-range .crm-clear-link{position:absolute;right:-20px;top:50%;transform:translateY(-50%)}#bootstrap-theme .civicase__activity-panel__core_container--draft [title='File On Case']{display:none}.crm-container.ui-dialog .ui-dialog-content.civicase__email-role-selector{height:220px!important}#bootstrap-theme .civicase__file-upload-container{margin:0 -12px}#bootstrap-theme .civicase__file-upload-item{padding:0 12px}#bootstrap-theme .civicase__file-upload-dropzone{align-items:center;border:1px dashed #c2cfd8;border-radius:3px;display:flex;flex-direction:column;height:330px;justify-content:center;padding:20px;width:100%}#bootstrap-theme .civicase__file-upload-dropzone:hover{background-color:#fff}#bootstrap-theme .civicase__file-upload-dropzone .material-icons{color:#bfcfd9;font-size:48px}#bootstrap-theme .civicase__file-upload-dropzone h3{font-size:14px;line-height:18px;margin:10px 0 0}#bootstrap-theme .civicase__file-upload-dropzone label{color:#0071bd;cursor:pointer;font-weight:400}#bootstrap-theme .civicase__file-upload-button{display:none!important}#bootstrap-theme .civicase__file-upload-box{transition:.25s width cubic-bezier(0,0,0,.4);width:100%}#bootstrap-theme .civicase__file-upload-details{opacity:0;overflow:hidden;padding:0;transition:.1s opacity cubic-bezier(0 0,0,.4);transition-delay:.3s;width:0}#bootstrap-theme .civicase__file-upload-details label{color:#9494a5;font-weight:400;line-height:18px;margin-bottom:5px}#bootstrap-theme .civicase__file-upload-details .btn{margin-right:8px;padding:8px 16px}#bootstrap-theme .civicase__file-upload-details .btn:last-child{margin-right:0}#bootstrap-theme .civicase__file-upload-details .btn-default{border-color:inherit}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector{margin-bottom:25px;margin-top:-10px}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7{float:none}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .select2-container{margin-bottom:5px;padding:0;width:100%!important}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-box{width:50%}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-details{opacity:1;overflow:visible;padding:0 12px;width:50%}#bootstrap-theme .civicase__file-upload-name,#bootstrap-theme .civicase__file-upload-remove,#bootstrap-theme .civicase__file-upload-size{font-size:13px;line-height:18px;margin:0}#bootstrap-theme .civicase__file-upload-name{color:#0071bd}#bootstrap-theme .civicase__file-upload-description{margin-bottom:24px}#bootstrap-theme .civicase__file-upload-description textarea{min-height:90px}#bootstrap-theme .civicase__file-upload-remove{text-align:right}#bootstrap-theme .civicase__file-upload-remove .btn{border:0;color:#cf3458;padding:0;text-transform:capitalize}#bootstrap-theme .civicase__file-upload-remove .btn:hover{background-color:transparent}#bootstrap-theme .civicase__file-upload-progress{margin:12px 0 16px}#bootstrap-theme .civicase__icon{transform:translateY(14%) rotate(.03deg)}#bootstrap-theme .civicase-inline-datepicker__wrapper{margin-left:-11px;margin-top:-5px}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]{border:1px solid transparent;border-radius:2px;height:30px;padding:4px 10px;width:140px!important}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control{box-shadow:none;display:inline-block}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control.ng-invalid{background-color:#fbe3e4}#bootstrap-theme .civicase-inline-datepicker__wrapper .addon{margin-top:-3px!important;opacity:0;transition:opacity .15s}#bootstrap-theme .civicase-inline-datepicker__wrapper:active [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:focus [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:hover [civicase-inline-datepicker]{border:1px solid #c2cfd8}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .form-control{box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .addon{opacity:1}#bootstrap-theme .civicase-inline-datepicker__wrapper .civicase__inline-datepicker--open{border:1px solid #c2cfd8;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:active+.addon,#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:focus+.addon{opacity:1}#bootstrap-theme .civicase__loading-placeholder__icon{color:#edf3f5;display:inline-block;font-size:10px;left:-14px;position:relative;top:-1px;width:10px}#bootstrap-theme .civicase__loading-placeholder__activity-card{border:1px solid #edf3f5;border-radius:4px;height:90px;margin-bottom:10px;position:relative;width:280px}#bootstrap-theme .civicase__loading-placeholder__activity-card::before{background-color:#edf3f5;border:.4em solid #fff;border-radius:1.5em;content:' ';font-size:1.5em;height:2.7em;left:1em;padding-top:.2em;position:absolute;text-align:center;top:.4em;width:2.7em}#bootstrap-theme .civicase__loading-placeholder__activity-card::after{background-color:#edf3f5;content:' ';height:30px;position:absolute;right:20px;top:20px;width:12px}#bootstrap-theme .civicase__loading-placeholder__activity-card div{margin-left:90px;margin-right:90px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::before{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::after{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__oneline::before{background-color:#edf3f5;content:' ';display:block;height:1em}#bootstrap-theme .civicase__loading-placeholder__date{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__date::before{border-left-color:#d6e4e8;border-top-color:#d6e4e8}#bootstrap-theme .civicase__loading-placeholder--big::before{height:1.5em}#bootstrap-theme .panel-header .civicase__loading-placeholder__oneline::before{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__oneline-strip{border-left-color:#edf3f5!important}#bootstrap-theme civicase-masonry-grid{width:100%}#bootstrap-theme civicase-masonry-grid .civicase__masonry-grid__column{float:left;width:50%}#bootstrap-theme civicase-masonry-grid-item{display:block}#bootstrap-theme .civicase__pager{background:#fafafb;border-radius:0 0 3px 3px;border-top:1px solid #e8eef0;padding:18px 15px;position:relative;z-index:10}#bootstrap-theme .civicase__pager .disabled{display:none}#bootstrap-theme .civicase__pager [title='First Page'] a,#bootstrap-theme .civicase__pager [title='Last Page'] a,#bootstrap-theme .civicase__pager [title='Next Page'] a,#bootstrap-theme .civicase__pager [title='Previous Page'] a{font-size:16px;font-weight:400;top:-3px}#bootstrap-theme .civicase__pager--fixed{bottom:0;left:0;position:fixed;width:100%}#bootstrap-theme .panel-query>.panel-body{transition:opacity .2s linear}#bootstrap-theme .panel-query.is-loading-page>.panel-body{opacity:.7}#bootstrap-theme .panel-query .civicase__activity-card--empty{text-align:center}#bootstrap-theme .panel-query .civicase__activity-card--big--empty-description{margin-bottom:0}#bootstrap-theme .panel-query .civicase__activity-no-result-icon{display:inline-block}#bootstrap-theme .panel-secondary{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .panel-secondary>.panel-body{padding:24px}#bootstrap-theme .panel-secondary>.panel-footer{padding:16px 24px}#bootstrap-theme .panel-secondary>.panel-heading{background:#fff;line-height:1;padding:24px;position:relative}#bootstrap-theme .panel-secondary>.panel-heading::after{border-bottom:1px solid #e8eef0;bottom:0;content:'';display:block;height:0;left:16px;position:absolute;width:calc(100% - 32px)}#bootstrap-theme .panel-secondary>.panel-heading .panel-title{font-size:16px}#bootstrap-theme .panel-secondary .panel-heading-control{display:block;margin-left:0;margin-top:-13px;position:relative;top:6px}#bootstrap-theme+.panel-secondary .panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary a.panel-heading-control{line-height:30px}#bootstrap-theme .panel-secondary .panel-title+.panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary .civicase__activity-card--long,#bootstrap-theme .panel-secondary .civicase__case-card--detached{box-shadow:0 3px 12px 0 rgba(49,40,40,.14);margin-bottom:0}#bootstrap-theme .panel-secondary .civicase__activity-card--long:not(:last-child),#bootstrap-theme .panel-secondary .civicase__case-card--detached:not(:last-child){margin-bottom:15px}#bootstrap-theme .civicase__panel-transparent-header{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading{background:0 0;padding:0}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:16px;margin:3px 0 12px}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading h3::before{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .civicase__pipe{color:#c2cfd8;font-weight:400;margin:0 5px}#bootstrap-theme .civicase__panel-transparent-header>.panel-body{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);padding:24px;position:relative}#bootstrap-theme civicase-popover{display:inline-block}#bootstrap-theme .civicase__popover-box{display:block}#bootstrap-theme civicase-popover-toggle-button{cursor:pointer}.civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}.civicase__tooltip-popup-list .popover-content{padding:0}.civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__responsive-calendar tbody,#bootstrap-theme .civicase__responsive-calendar thead{display:block}#bootstrap-theme .civicase__responsive-calendar tr{display:flex}#bootstrap-theme .civicase__responsive-calendar td,#bootstrap-theme .civicase__responsive-calendar th{padding:0;width:100%}#bootstrap-theme .civicase__show-more-button{cursor:pointer}#bootstrap-theme .civicase__summary-tab__basic-details{background:#fff}#bootstrap-theme .civicase__summary-tab__basic-details .panel-body{display:flex;flex-direction:row;padding:20px}#bootstrap-theme .civicase__summary-tab__subject-container{flex-basis:56%;padding-right:15px}#bootstrap-theme .civicase__summary-tab__subject{font-size:20px;font-weight:600;line-height:27px;margin:0 0 13px}#bootstrap-theme .civicase__summary-tab__description{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-tab__last-updated{margin:13px 0 0;padding:2px 4px}#bootstrap-theme .civicase__summary-tab__last-updated__label{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-activity-count{align-items:center;border-left:1px solid #e8eef0;color:#464354;display:table-cell;font-weight:600;justify-content:center;text-align:center;vertical-align:top;width:20%}#bootstrap-theme .civicase__summary-activity-count a,#bootstrap-theme .civicase__summary-activity-count a:hover{color:#464354;text-decoration:none}#bootstrap-theme .civicase__summary-activity-count a{display:block;height:100%;margin:0 10px}#bootstrap-theme .civicase__summary-activity-count a:hover{background:#f3f6f7;border-radius:5px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__number{font-size:50px;line-height:76px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__description{color:#9494a5;font-size:15px;line-height:22px}#bootstrap-theme .civicase__summary-overdue-count{line-height:18px}#bootstrap-theme .civicase__summary-tab-tile{margin-bottom:15px;padding:15px}#bootstrap-theme .civicase__summary-tab-tile>.panel{margin-bottom:0}#bootstrap-theme .civicase__summary-tab-tile>.panel-body{border-radius:5px;border-top:0!important;padding:0}#bootstrap-theme .civicase__summary-tab-tile .civicase__panel-transparent-header>.panel-body{border-radius:5px}#bootstrap-theme .civicase__summary-tab-tile-container{display:flex;padding:0 15px;width:100%}#bootstrap-theme .civicase__summary-tab-tile--fixed{width:350px}#bootstrap-theme .civicase__summary-tab-tile--responsive{flex-basis:calc(50% - 150px);flex-grow:1;min-width:0}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-heading .civicase__case-card__activity-count:nth-child(2){margin-left:10px}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-body{background:0 0;box-shadow:none}#bootstrap-theme .civicase__summary-tab__activity-list .civicase__activity-card{margin-bottom:15px}#bootstrap-theme .civicase__summary-tab__other-cases{margin-left:25px;margin-right:25px}#bootstrap-theme .civicase__summary-tab__other-cases>.panel-collapse>.panel-body{padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .panel-footer{border-top:0;padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager{border-top:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::after,#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::before{display:none}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li[title^=Page].active>a{color:#464354}#crm-container .civicase__tabs{background-color:#fff;padding:0 0 0 20px}#crm-container .civicase__tabs .ui-tab{background:0 0;border:0;padding:0}#crm-container .civicase__tabs .ui-tab .ui-tabs-anchor{height:auto;padding:15px 20px!important}#crm-container .civicase__tabs__panel{padding:15px 20px}#bootstrap-theme .civicase__tags-container .badge,#bootstrap-theme .civicase__tags-container__additional__list .badge{margin-right:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__tags-container{display:inline-block}#bootstrap-theme .civicase__tags-container .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional__list{margin:0;padding:0}#bootstrap-theme .civicase__tags-container__additional__list li{list-style:none;padding:5px 10px}#bootstrap-theme .civicase__tags-container__additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__tags-container__additional__list li a{text-decoration:none}#bootstrap-theme .civicase__tags-container__additional__list .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional-tags{background-color:#9494a5;display:inline;padding:0 8px}#bootstrap-theme .civicase__tags-container__additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer;max-width:calc(36ch + 20px + 4px);padding:0}#bootstrap-theme .civicase__tags-container__additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__tags-container__additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tags-container__additional__popover .popover-content{background:#fff;padding:0}.civicase__tags-modal{background-color:#fff!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container{border:1px solid #d3dee2;border-radius:2px;padding:5px}#bootstrap-theme .civicase__tags-modal__generic-tags-container input{margin-top:0!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container label{margin-bottom:0}#bootstrap-theme .civicase__tags-modal__tags-container label{margin-top:4px}#bootstrap-theme .civicase__tags-modal__tags-container .col-sm-9{width:75%!important}.civicase__tags-selector__item-color{margin-right:2px;position:relative;top:1px}#bootstrap-theme .civicase__tooltip__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;z-index:5}#bootstrap-theme .civicase__tooltip__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tooltip__popup .arrow.left{left:20px}#bootstrap-theme .civicase__tooltip__ellipsis{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__pipe{position:relative;top:-1px}#bootstrap-theme .civicase__text-warning{color:#e6ab5e}#bootstrap-theme .civicase__text-success{color:#44cb7e}#bootstrap-theme .civicase__link-disabled{cursor:no-drop;pointer-events:none}#bootstrap-theme .civicase__spinner{animation:spin 1.5s linear infinite;background:url(../resources/icons/spinner.svg) no-repeat center center!important;display:block;height:32px;margin:auto;width:32px}#bootstrap-theme .civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}#bootstrap-theme .civicase__tooltip-popup-list .popover-content{padding:0}#bootstrap-theme .civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase-workflow-list>.panel-body{padding:0}#bootstrap-theme .civicase-workflow-list__new-button{margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__new-button .material-icons{position:relative;top:2px}#bootstrap-theme .civicase-workflow-list_duplicate-form .ng-invalid:not(.ng-untouched){border-color:#cf3458}#bootstrap-theme .civicase-workflow-list_duplicate-form textarea{width:100%}#bootstrap-theme .civicase-workflow-list__filters .form-group{margin-bottom:0}#bootstrap-theme .civicase-workflow-list__filters .form-group [type=checkbox]{margin:0}#bootstrap-theme .civicase-workflow-list__filters .form-group>div{display:inline-block;margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(odd){margin-right:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(even){margin-left:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group .select2-container{width:240px!important}#civicaseActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)}.page-civicrm-contact-view:not([class*=page-civicrm-contact-view-]) #crm-container .civicase__activity-panel__core_container .crm-submit-buttons{margin-bottom:0!important}.page-civicrm-case-a #page{margin:0;padding-top:0}.page-civicrm-case-a .block-civicrm>h2{margin:0}.page-civicrm-case-a #branding{padding:16px 0!important}.page-civicrm-case-a #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-case-a #branding .breadcrumb>a{left:0}@media (max-width:1455px){.page-civicrm-case-a .crm-contactEmail-form-block-subject .crm-token-selector{margin-top:5px}}.page-civicrm-dashboard #page,.page-civicrm:not([class*=' page-civicrm-']) #page{margin:0;padding-top:0}.page-civicrm-dashboard .block-civicrm>h2,.page-civicrm:not([class*=' page-civicrm-']) .block-civicrm>h2{margin:0}.page-civicrm-dashboard #branding,.page-civicrm:not([class*=' page-civicrm-']) #branding{padding:16px 0!important}.page-civicrm-dashboard #branding .breadcrumb,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-dashboard #branding .breadcrumb>a,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb>a{left:0}.page-civicrm-dashboard .civicase__tabs.affix,.page-civicrm:not([class*=' page-civicrm-']) .civicase__tabs.affix{position:fixed;top:0;width:100%;z-index:9999}.civicase__crm-dashboard .tab-content,.civicase__crm-dashboard.ui-tabs{background:0 0}#civicaseMyActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseMyActivitiesTab [crm-page-title]{display:none}#civicaseMyActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseMyActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseMyActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)} /*# sourceMappingURL=civicase.min.css.map */ diff --git a/info.xml b/info.xml index 731ac3434..fb74e2dbe 100644 --- a/info.xml +++ b/info.xml @@ -25,13 +25,25 @@ CiviCRM 5.51.1 - + + CRM/Civicase + 23.02.1 org.civicrm.shoreditch uk.co.compucorp.usermenu + org.civicrm.afform + + ang-php@1.0.0 + menu-xml@1.0.0 + mgd-php@1.0.0 + setting-php@1.0.0 + smarty-v2@1.0.1 + entity-types-php@1.0.0 + + CRM_Civicase_Upgrader diff --git a/js/contribution-entityref-field.js b/js/contribution-entityref-field.js new file mode 100644 index 000000000..8d0922353 --- /dev/null +++ b/js/contribution-entityref-field.js @@ -0,0 +1,30 @@ +(function ($, _) { + window.waitForElement = function ($, elementPath, callBack) { + (new window.MutationObserver(function () { + callBack($, $(elementPath)); + })).observe(document.querySelector(elementPath), { + attributes: true + }); + }; + + $(document).one('crmLoad', function () { + const entityRefCustomFields = Object.values(CRM.vars.civicase.entityRefCustomFields ?? {}); + + /* eslint-disable no-undef */ + waitForElement($, '#customData', function ($, elem) { + entityRefCustomFields.forEach(field => { + $(`[name^=${field.name}_]`) + .attr('placeholder', field.placeholder) + .attr('disabled', false) + .crmEntityRef({ + entity: field.entity, + create: false + }); + + if (field.value) { + $(`[name^=${field.name}_]`).val(field.value).trigger('change'); + } + }); + }); + }); +})(CRM.$, CRM._); diff --git a/js/invoice-bulk-mail.js b/js/invoice-bulk-mail.js new file mode 100644 index 000000000..d63bb1bac --- /dev/null +++ b/js/invoice-bulk-mail.js @@ -0,0 +1,5 @@ +(function ($, _) { + $('input[value=email_invoice]').prop('checked', true).click(); + $('div.help').hide(); + $('input[name=output]').parent().parent().hide(); +})(CRM.$, CRM._); diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js new file mode 100644 index 000000000..5de753872 --- /dev/null +++ b/js/sales-order-contribution.js @@ -0,0 +1,84 @@ +(function ($, _) { + const waitForElement = function ($, elementPath, callBack) { + (new window.MutationObserver(function () { + callBack($, $(elementPath)); + })).observe(document.querySelector(elementPath), { + attributes: true + }); + }; + + $(document).one('crmLoad', function () { + const params = CRM.vars['uk.co.compucorp.civicase']; + const salesOrderId = params.sales_order; + const salesOrderStatusId = params.sales_order_status_id; + const percentValue = params.percent_value; + const toBeInvoiced = params.to_be_invoiced; + const lineItems = JSON.parse(params.line_items); + const caseCustomField = params.case_custom_field; + const quotationCustomField = params.quotation_custom_field; + let count = 0; + + const apiRequest = {}; + apiRequest.caseSalesOrders = ['CaseSalesOrder', 'get', { + select: ['*'], + where: [['id', '=', salesOrderId]] + }]; + apiRequest.optionValues = ['OptionValue', 'get', { + select: ['value'], + where: [['option_group_id:name', '=', 'contribution_status'], ['name', '=', 'pending']] + }]; + + if (Array.isArray(lineItems)) { + CRM.api4(apiRequest).then(function (batch) { + const caseSalesOrder = batch.caseSalesOrders[0]; + + lineItems.forEach(lineItem => { + addLineItem(lineItem.qty, lineItem.unit_price, lineItem.label, lineItem.financial_type_id, lineItem.tax_amount); + }); + + $('#total_amount').val(0); + $('#lineitem-add-block').show().removeClass('hiddenElement'); + $('#contribution_status_id').val(batch.optionValues[0].value); + $('#source').val(`Quotation ${caseSalesOrder.id}`).trigger('change'); + $('#contact_id').select2('val', caseSalesOrder.client_id).trigger('change'); + $('input[id="total_amount"]', 'form.CRM_Contribute_Form_Contribution').trigger('change'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $('#totalAmount, #totalAmountORaddLineitem, #totalAmountORPriceSet, #price_set_id, #choose-manual').hide(); + + waitForElement($, '#customData', function ($, elem) { + $(`[name^=${caseCustomField}_]`).val(caseSalesOrder.case_id).trigger('change'); + $(`[name^=${quotationCustomField}_]`).val(caseSalesOrder.id).trigger('change'); + }); + }); + } + + /** + * @param {number} quantity Item quantity + * @param {number} unitPrice Item unit price + * @param {string} description Item description + * @param {number} financialTypeId Item financial type + * @param {number|object} taxAmount Item tax amount + */ + function addLineItem (quantity, unitPrice, description, financialTypeId, taxAmount) { + const row = $($(`tr#add-item-row-${count}`)); + row.show().removeClass('hiddenElement'); + quantity = +parseFloat(quantity).toFixed(10); // limit to 10 decimal places + + $('input[id^="item_label"]', row).val(ts(description)); + $('select[id^="item_financial_type_id"]', row).select2('val', financialTypeId); + $('input[id^="item_qty"]', row).val(quantity); + + const total = quantity * parseFloat(unitPrice); + + $('input[id^="item_unit_price"]', row).val(unitPrice); + $('input[id^="item_line_total"]', row).val(CRM.formatMoney(total, true)); + + $('input[id^="item_tax_amount"]', row).val(taxAmount); + + count++; + } + }); +})(CRM.$, CRM._); diff --git a/managed/CustomGroup_Cases_OpportunityDetails.mgd.php b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php new file mode 100644 index 000000000..2241110ea --- /dev/null +++ b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php @@ -0,0 +1,234 @@ + 'CustomGroup_Case_Opportunity_Details', + 'entity' => 'CustomGroup', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Case_Opportunity_Details', + 'title' => 'Opportunity Details', + 'extends' => 'Case', + 'extends_entity_column_value' => NULL, + 'style' => 'Inline', + 'collapse_display' => FALSE, + 'help_pre' => '', + 'help_post' => '', + 'weight' => 70, + 'is_active' => TRUE, + 'is_multiple' => FALSE, + 'min_multiple' => NULL, + 'max_multiple' => NULL, + 'collapse_adv_display' => TRUE, + 'created_date' => '2023-09-21 14:46:02', + 'is_reserved' => FALSE, + 'is_public' => FALSE, + 'icon' => '', + 'extends_entity_column_id' => NULL, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Quoted', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Quoted', + 'label' => 'Total Amount Quoted', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_quoted', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Invoiced', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Invoiced', + 'label' => 'Total Amount Invoiced', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_invoiced', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Invoicing_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Invoicing_Status', + 'label' => 'Invoicing Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'invoicing_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_amounts_paid', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amounts_Paid', + 'label' => 'Total Amounts Paid', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amounts_paid', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Payments_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Payments_Status', + 'label' => 'Payments Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'payments_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], +]; diff --git a/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php b/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php new file mode 100644 index 000000000..69534234a --- /dev/null +++ b/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php @@ -0,0 +1,117 @@ + 'CustomGroup_Opportunity_Details', + 'entity' => 'CustomGroup', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Opportunity_Details', + 'title' => 'Opportunity Details', + 'extends' => 'Contribution', + 'extends_entity_column_value' => NULL, + 'style' => 'Inline', + 'collapse_display' => TRUE, + 'help_pre' => '

If using CiviProspect you can link this contribution to a prospect, quotation or both below.

', + 'help_post' => '', + 'weight' => 34, + 'is_active' => TRUE, + 'is_multiple' => FALSE, + 'min_multiple' => NULL, + 'max_multiple' => NULL, + 'collapse_adv_display' => TRUE, + 'created_date' => '2023-09-12 13:08:59', + 'is_reserved' => FALSE, + 'is_public' => FALSE, + 'icon' => '', + 'extends_entity_column_id' => NULL, + ], + ], + ], + [ + 'name' => 'CustomGroup_Opportunity_Details_CustomField_Case_Opportunity', + 'entity' => 'CustomField', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Opportunity_Details', + 'name' => 'Case_Opportunity', + 'label' => 'Case/Opportunity', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => FALSE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'case_opportunity_51', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Opportunity_Details_CustomField_Quotation', + 'entity' => 'CustomField', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Opportunity_Details', + 'name' => 'Quotation', + 'label' => 'Quotation', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => FALSE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'quotation_52', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], +]; diff --git a/managed/CustomGroup_Product_Discounts.mgd.php b/managed/CustomGroup_Product_Discounts.mgd.php new file mode 100644 index 000000000..664d3fe72 --- /dev/null +++ b/managed/CustomGroup_Product_Discounts.mgd.php @@ -0,0 +1,94 @@ + 'CustomGroup_Product_Discounts', + 'entity' => 'CustomGroup', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Product_Discounts', + 'title' => 'Product Discounts', + 'extends' => 'MembershipType', + 'extends_entity_column_value' => NULL, + 'style' => 'Inline', + 'collapse_display' => FALSE, + 'help_pre' => '', + 'help_post' => '', + 'weight' => 107, + 'is_active' => TRUE, + 'is_multiple' => FALSE, + 'min_multiple' => NULL, + 'max_multiple' => NULL, + 'collapse_adv_display' => TRUE, + 'created_date' => '2023-08-25 07:22:08', + 'is_reserved' => FALSE, + 'is_public' => TRUE, + 'icon' => '', + 'extends_entity_column_id' => NULL, + ], + ], + ], + [ + 'name' => 'CustomGroup_Product_Discounts_CustomField_Product_Discount_Amount', + 'entity' => 'CustomField', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Product_Discounts', + 'name' => 'Product_Discount_Amount', + 'label' => 'Product Discount Amount', + 'data_type' => 'Float', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => 'Specify a discount that will automatically be applied when adding a product line item to a quotation if the contact is a member of this type.', + 'mask' => NULL, + 'attributes' => ' pattern="([0-9]*[.])?[0-9]+" ', + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => FALSE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'product_discount_amount', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], +]; + +$rowCount = OptionValue::get(FALSE) + ->selectRowCount() + ->addSelect('*') + ->addWhere('option_group_id:name', '=', 'cg_extend_objects') + ->addWhere('name', '=', 'civicrm_membership_type') + ->execute() + ->count(); + +if ($rowCount == 1) { + return $mgd; +} + +return []; diff --git a/managed/SavedSearch_Civicase_Quotations.mgd.php b/managed/SavedSearch_Civicase_Quotations.mgd.php new file mode 100644 index 000000000..1000f31fc --- /dev/null +++ b/managed/SavedSearch_Civicase_Quotations.mgd.php @@ -0,0 +1,609 @@ + 'SavedSearch_Civicase_Quotations', + 'entity' => 'SavedSearch', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Civicase_Quotations', + 'label' => 'Civicase Quotations', + 'form_values' => NULL, + 'search_custom_id' => NULL, + 'api_entity' => 'CaseSalesOrder', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'id', + 'client_id.display_name', + 'DATE(quotation_date) AS DATE_quotation_date', + 'description', + 'owner_id.display_name', + 'CONCAT_WS(" ", total_before_tax, currency:label) AS CONCAT_WS_total_before_tax_currency_label', + 'CONCAT_WS(" ", total_after_tax, currency:label) AS CONCAT_WS_total_after_tax_currency_label', + 'status_id:label', + 'invoicing_status_id:label', + 'payment_status_id:label', + ], + 'orderBy' => [ + 'created_at' => 'DESC', + ], + 'where' => [], + 'groupBy' => [], + 'join' => [], + 'having' => [], + 'limit' => 10, + ], + 'expires_date' => NULL, + 'description' => NULL, + 'mapping_id' => NULL, + ], + ], + ], + [ + 'name' => 'SavedSearch_Civicase_Quotations_SearchDisplay_Civicase_Contact_Quotations_Table', + 'entity' => 'SearchDisplay', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Civicase_Contact_Quotations_Table', + 'label' => 'Contact Quotations List', + 'saved_search_id.name' => 'Civicase_Quotations', + 'type' => 'table', + 'settings' => [ + 'actions' => TRUE, + 'limit' => 10, + 'classes' => [ + 'table', + 'table-striped', + ], + 'pager' => [], + 'placeholder' => 5, + 'sort' => [ + [ + 'created_at', + 'DESC', + ], + ], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'id', + 'dataType' => 'Integer', + 'label' => 'ID', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'DATE_quotation_date', + 'dataType' => 'Date', + 'label' => 'Date', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'owner_id.display_name', + 'dataType' => 'String', + 'label' => 'Owner', + 'sortable' => TRUE, + 'link' => [ + 'path' => '', + 'entity' => 'Contact', + 'action' => 'view', + 'join' => 'owner_id', + 'target' => '_blank', + ], + 'title' => 'View Owner', + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'CONCAT_WS_total_before_tax_currency_label', + 'dataType' => 'String', + 'label' => 'Total Before Tax', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'CONCAT_WS_total_after_tax_currency_label', + 'dataType' => 'String', + 'label' => 'Total After Tax', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'invoicing_status_id:label', + 'dataType' => 'Integer', + 'label' => 'Invoicing', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'payment_status_id:label', + 'dataType' => 'Integer', + 'label' => 'Payments', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'status_id:label', + 'dataType' => 'Integer', + 'label' => 'Status', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'text' => '', + 'style' => 'default', + 'size' => 'btn-sm', + 'icon' => 'fa-bars', + 'links' => [ + [ + 'path' => '', + 'icon' => 'fa-external-link', + 'text' => 'View', + 'style' => 'default', + 'condition' => [], + 'entity' => 'CaseSalesOrder', + 'action' => 'view', + 'join' => '', + 'target' => '_blank', + ], + [ + 'entity' => 'CaseSalesOrder', + 'action' => 'update', + 'join' => '', + 'target' => '_blank', + 'icon' => 'fa-pencil', + 'text' => 'Edit', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'entity' => 'CaseSalesOrder', + 'action' => 'delete', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-trash', + 'text' => 'Delete', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'path' => 'civicrm/case-features/quotations/download-pdf?id=[id]', + 'icon' => 'fa-file-pdf-o', + 'text' => 'Download PDF', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => '_blank', + ], + [ + 'path' => 'civicrm/case-features/quotations/create-contribution?id=[id]', + 'icon' => 'fa-credit-card', + 'text' => 'Create Contribution', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => 'crm-popup', + ], + [ + 'path' => 'civicrm/case-features/quotations/email?id=[id]', + 'icon' => 'fa-paper-plane-o', + 'text' => 'Send By Email', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => 'crm-popup', + ], + ], + 'type' => 'menu', + 'alignment' => '', + 'label' => 'Actions', + ], + ], + 'noResultsText' => 'No quotations found', + ], + 'acl_bypass' => FALSE, + ], + ], + ], + [ + 'name' => 'SavedSearch_Civicase_Quotations_SearchDisplay_Civicase_Quotations_Table', + 'entity' => 'SearchDisplay', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Civicase_Quotations_Table', + 'label' => 'Quotations List', + 'saved_search_id.name' => 'Civicase_Quotations', + 'type' => 'table', + 'settings' => [ + 'actions' => TRUE, + 'limit' => 10, + 'classes' => [ + 'table', + 'table-striped', + ], + 'pager' => [], + 'placeholder' => 5, + 'sort' => [ + [ + 'created_at', + 'DESC', + ], + ], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'id', + 'dataType' => 'Integer', + 'label' => 'ID', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'client_id.display_name', + 'dataType' => 'String', + 'label' => 'Client', + 'sortable' => TRUE, + 'link' => [ + 'path' => '', + 'entity' => 'Contact', + 'action' => 'view', + 'join' => 'client_id', + 'target' => '_blank', + ], + 'title' => 'View Client', + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'DATE_quotation_date', + 'dataType' => 'Date', + 'label' => 'Date', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'owner_id.display_name', + 'dataType' => 'String', + 'label' => 'Owner', + 'sortable' => TRUE, + 'link' => [ + 'path' => '', + 'entity' => 'Contact', + 'action' => 'view', + 'join' => 'owner_id', + 'target' => '_blank', + ], + 'title' => 'View Owner', + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'CONCAT_WS_total_before_tax_currency_label', + 'dataType' => 'String', + 'label' => 'Total Before Tax', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'CONCAT_WS_total_after_tax_currency_label', + 'dataType' => 'String', + 'label' => 'Total After Tax', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'status_id:label', + 'dataType' => 'Integer', + 'label' => 'Status', + 'sortable' => TRUE, + 'alignment' => '', + ], + [ + 'type' => 'field', + 'key' => 'invoicing_status_id:label', + 'dataType' => 'Integer', + 'label' => 'Invoicing', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'payment_status_id:label', + 'dataType' => 'Integer', + 'label' => 'Payments', + 'sortable' => TRUE, + ], + [ + 'text' => '', + 'style' => 'default', + 'size' => 'btn-sm', + 'icon' => 'fa-bars', + 'links' => [ + [ + 'path' => '', + 'icon' => 'fa-external-link', + 'text' => 'View', + 'style' => 'default', + 'condition' => [], + 'entity' => 'CaseSalesOrder', + 'action' => 'view', + 'join' => '', + 'target' => '_blank', + ], + [ + 'entity' => 'CaseSalesOrder', + 'action' => 'update', + 'join' => '', + 'target' => '_blank', + 'icon' => 'fa-pencil', + 'text' => 'Edit', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'entity' => 'CaseSalesOrder', + 'action' => 'delete', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-trash', + 'text' => 'Delete', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'path' => 'civicrm/case-features/quotations/download-pdf?id=[id]', + 'icon' => 'fa-file-pdf-o', + 'text' => 'Download PDF', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => '_blank', + ], + [ + 'path' => 'civicrm/case-features/quotations/create-contribution?id=[id]', + 'icon' => 'fa-credit-card', + 'text' => 'Create Contribution', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => 'crm-popup', + ], + [ + 'path' => 'civicrm/case-features/quotations/email?id=[id]', + 'icon' => 'fa-paper-plane-o', + 'text' => 'Send By Email', + 'style' => 'default', + 'condition' => [], + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => 'crm-popup', + ], + ], + 'type' => 'menu', + 'alignment' => '', + 'label' => 'Actions', + ], + ], + 'noResultsText' => 'No quotations found', + ], + 'acl_bypass' => FALSE, + ], + ], + ], + [ + 'name' => 'SavedSearch_Quotation_Contributions', + 'entity' => 'SavedSearch', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Quotation_Contributions', + 'label' => 'Quotation Contributions', + 'form_values' => NULL, + 'search_custom_id' => NULL, + 'api_entity' => 'Contribution', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'total_amount', + 'financial_type_id:label', + 'source', + 'receive_date', + 'thankyou_date', + 'contribution_status_id:label', + 'Opportunity_Details.Case_Opportunity', + ], + 'orderBy' => [], + 'where' => [ + [ + 'id', + 'IS NOT EMPTY', + ], + [ + 'OR', + [ + [ + 'Opportunity_Details.Case_Opportunity', + 'IS NOT EMPTY', + ], + [ + 'Opportunity_Details.Quotation', + 'IS NOT EMPTY', + ], + ], + ], + ], + 'groupBy' => [], + 'join' => [], + 'having' => [], + ], + 'expires_date' => NULL, + 'description' => NULL, + 'mapping_id' => NULL, + ], + ], + ], + [ + 'name' => 'SavedSearch_Quotation_Contributions_SearchDisplay_Contributions_Table_1', + 'entity' => 'SearchDisplay', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Contributions_Table_1', + 'label' => 'Contributions Table 1', + 'saved_search_id.name' => 'Quotation_Contributions', + 'type' => 'table', + 'settings' => [ + 'actions' => TRUE, + 'limit' => 10, + 'classes' => [ + 'table', + 'table-striped', + ], + 'pager' => [], + 'placeholder' => 5, + 'sort' => [], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'total_amount', + 'dataType' => 'Money', + 'label' => 'Amount', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'financial_type_id:label', + 'dataType' => 'Integer', + 'label' => 'Type', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'source', + 'dataType' => 'String', + 'label' => 'Source', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'receive_date', + 'dataType' => 'Timestamp', + 'label' => 'Date', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'thankyou_date', + 'dataType' => 'Timestamp', + 'label' => 'Thank-you Sent', + 'sortable' => TRUE, + 'rewrite' => '{if "[thankyou_date]"} Yes {else} No {/if}', + ], + [ + 'type' => 'field', + 'key' => 'contribution_status_id:label', + 'dataType' => 'Integer', + 'label' => 'Status', + 'sortable' => TRUE, + ], + [ + 'text' => 'Actions', + 'style' => 'default', + 'size' => 'btn-sm', + 'icon' => 'fa-bars', + 'links' => [ + [ + 'entity' => 'Contribution', + 'action' => 'view', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-external-link', + 'text' => 'View', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'entity' => 'Contribution', + 'action' => 'update', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-pencil', + 'text' => 'Edit', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-paper-plane-o', + 'text' => 'Send By Email', + 'style' => 'default', + 'path' => 'civicrm/contribute/invoice/email/?reset=1&id=[id]&select=email&cid=[contact_id]', + 'condition' => [], + ], + ], + 'type' => 'menu', + 'alignment' => 'text-right', + ], + ], + 'noResultsText' => 'No Invoices found', + ], + 'acl_bypass' => FALSE, + ], + ], + ], +]; + +$searchKitIsInstalled = 'installed' === +CRM_Extension_System::singleton()->getManager()->getStatus('org.civicrm.search_kit'); +if ($searchKitIsInstalled) { + return $mgd; +} + +return []; diff --git a/mixin/entity-types-php@1.0.0.mixin.php b/mixin/entity-types-php@1.0.0.mixin.php new file mode 100644 index 000000000..8ec4203df --- /dev/null +++ b/mixin/entity-types-php@1.0.0.mixin.php @@ -0,0 +1,36 @@ +addListener('hook_civicrm_entityTypes', function ($e) use ($mixInfo) { + // When deactivating on a polyfill/pre-mixin system, listeners may not cleanup automatically. + if (!$mixInfo->isActive() || !is_dir($mixInfo->getPath('xml/schema/CRM'))) { + return; + } + + $files = (array) glob($mixInfo->getPath('xml/schema/CRM/*/*.entityType.php')); + foreach ($files as $file) { + $entities = include $file; + foreach ($entities as $entity) { + $e->entityTypes[$entity['class']] = $entity; + } + } + }); + +}; diff --git a/mixin/smarty-v2@1.0.1.mixin.php b/mixin/smarty-v2@1.0.1.mixin.php new file mode 100644 index 000000000..5972dbdc5 --- /dev/null +++ b/mixin/smarty-v2@1.0.1.mixin.php @@ -0,0 +1,51 @@ +getPath('templates'); + if (!file_exists($dir)) { + return; + } + + $register = function() use ($dir) { + // This implementation has a theoretical edge-case bug on older versions of CiviCRM where a template could + // be registered more than once. + CRM_Core_Smarty::singleton()->addTemplateDir($dir); + }; + + // Let's figure out what environment we're in -- so that we know the best way to call $register(). + + if (!empty($GLOBALS['_CIVIX_MIXIN_POLYFILL'])) { + // Polyfill Loader (v<=5.45): We're already in the middle of firing `hook_config`. + if ($mixInfo->isActive()) { + $register(); + } + return; + } + + if (CRM_Extension_System::singleton()->getManager()->extensionIsBeingInstalledOrEnabled($mixInfo->longName)) { + // New Install, Standard Loader: The extension has just been enabled, and we're now setting it up. + // System has already booted. New templates may be needed for upcoming installation steps. + $register(); + return; + } + + // Typical Pageview, Standard Loader: Defer the actual registration for a moment -- to ensure that Smarty is online. + \Civi::dispatcher()->addListener('hook_civicrm_config', function() use ($mixInfo, $register) { + if ($mixInfo->isActive()) { + $register(); + } + }); + +}; diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml index 0524d7a5d..7a721e9d4 100644 --- a/phpcs-ruleset.xml +++ b/phpcs-ruleset.xml @@ -15,12 +15,16 @@ tests/phpunit/bootstrap.php + mixin/* CRM/Civicase/Form/Report/ExtendedReport.php civicase.civix.php CRM/Civicase/DAO/* CRM/Civicase/Upgrader/Base.php CRM/Civicase/Form/Report/Case/CaseWithActivityPivot.php + tests/phpunit/api/v3/Case/BaseTestCase.php + tests/phpunit/BaseHeadlessTest.phpp + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0f9f25d30..fc8f870b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + ./tests/phpunit diff --git a/scss/components/_case-features.scss b/scss/components/_case-features.scss new file mode 100644 index 000000000..5690ee6c4 --- /dev/null +++ b/scss/components/_case-features.scss @@ -0,0 +1,4 @@ +.civicase__features-filters { + display: flex; + justify-content: space-between; +} diff --git a/sql/auto_install.sql b/sql/auto_install.sql index 9a3244980..c85f5274a 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -29,3 +29,74 @@ CREATE TABLE IF NOT EXISTS `civicrm_case_category_instance` ( PRIMARY KEY (`id`), UNIQUE INDEX `unique_category`(category_id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; + +-- /******************************************************* +-- * +-- * civicrm_case_category_features +-- * +-- * Stores additional features enabled for a case category +-- * +-- *******************************************************/ +CREATE TABLE IF NOT EXISTS `civicrm_case_category_features` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseCategoryFeatures ID', + `category_id` int unsigned NOT NULL COMMENT 'One of the values of the case_type_categories option group', + `feature_id` int unsigned NOT NULL COMMENT 'One of the values of the case_type_category_features option group', + PRIMARY KEY (`id`), + UNIQUE INDEX `unique_category_feature` (category_id, feature_id) +) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; + + +-- /******************************************************* +-- * +-- * civicase_sales_order +-- * +-- * Sales order that represents quotations +-- * +-- *******************************************************/ +CREATE TABLE IF NOT EXISTS `civicase_sales_order` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrder ID', + `client_id` int unsigned COMMENT 'FK to Contact', + `owner_id` int unsigned COMMENT 'FK to Contact', + `case_id` int unsigned COMMENT 'FK to Case', + `currency` varchar(3) DEFAULT NULL COMMENT '3 character string, value from config setting or input via user.', + `status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_status option group', + `invoicing_status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_invoicing_status option group', + `payment_status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_payment_status option group', + `description` text NULL COMMENT 'Sales order deesctiption', + `notes` text NULL COMMENT 'Sales order notes', + `total_before_tax` decimal(20,2) NULL COMMENT 'Total amount of the sales order line items before tax deduction.', + `total_after_tax` decimal(20,2) NULL COMMENT 'Total amount of the sales order line items after tax deduction.', + `quotation_date` timestamp COMMENT 'Quotation date', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT 'Date the sales order is created', + `is_deleted` tinyint DEFAULT 0 COMMENT 'Is this sales order deleted?', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicase_sales_order_client_id FOREIGN KEY (`client_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_owner_id FOREIGN KEY (`owner_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_case_id FOREIGN KEY (`case_id`) REFERENCES `civicrm_case`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicase_sales_order_line +-- * +-- * Sales order line items +-- * +-- *******************************************************/ +CREATE TABLE IF NOT EXISTS `civicase_sales_order_line` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrderLine ID', + `sales_order_id` int unsigned COMMENT 'FK to CaseSalesOrder', + `financial_type_id` int unsigned COMMENT 'FK to CiviCRM Financial Type', + `product_id` int unsigned, + `item_description` text NULL COMMENT 'line item deesctiption', + `quantity` decimal(20,2) COMMENT 'Quantity', + `unit_price` decimal(20,2) COMMENT 'Unit Price', + `tax_rate` decimal(20,2) COMMENT 'Tax rate for the line item', + `discounted_percentage` decimal(20,2) COMMENT 'Discount applied to the line item', + `subtotal_amount` decimal(20,2) COMMENT 'Quantity x Unit Price x (100-Discount)%', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicase_sales_order_line_sales_order_id FOREIGN KEY (`sales_order_id`) REFERENCES `civicase_sales_order`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_line_financial_type_id FOREIGN KEY (`financial_type_id`) REFERENCES `civicrm_financial_type`(`id`) ON DELETE SET NULL, + CONSTRAINT FK_civicase_sales_order_line_product_id FOREIGN KEY (`product_id`) REFERENCES `civicrm_product`(`id`) ON DELETE SET NULL +) +ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index af9bba66c..80fb0b568 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -1,7 +1,24 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +---- /******************************************************* +-- * +-- * Clean up the existing tables-- * +-- *******************************************************/ + SET FOREIGN_KEY_CHECKS=0; +DROP TABLE IF EXISTS `civicase_sales_order_line`; +DROP TABLE IF EXISTS `civicase_sales_order`; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; -ALTER TABLE `civicrm_case_type` DROP COLUMN `case_type_category`; +DROP TABLE IF EXISTS `civicrm_case_category_features`; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html b/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html index 1d510af43..4798c4dc5 100644 --- a/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html +++ b/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html @@ -9,7 +9,7 @@ api: { params: {option_group_id: 'case_type_categories'} }, - select: {allowClear: true, multiple: false, placeholder: ts('Select Instance')}, + select: {allowClear: true, multiple: false, placeholder: ts('Select Instance'), 'minimumInputLength': 0}, }" class="big crm-form-text" /> diff --git a/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl b/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl new file mode 100644 index 000000000..be04a172b --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl @@ -0,0 +1,22 @@ + + + + + + + +
Additional Features + + {foreach from=$features item=row} + + + + {/foreach} +
{$form.$row.html} {$form.$row.label}
+
diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl new file mode 100644 index 000000000..d575b8bae --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl @@ -0,0 +1,63 @@ +
+ +
+
+
+
+
+ {$form.to_be_invoiced.percent.html} +
+
+ {$form.percent_value.html} +
+
+
+ +
+
+
+ {$form.to_be_invoiced.remain.html} +
+
+
+ +
+
+ +
+ {$form.status.html} +
+
+
+
+
+ + +
+ {include file="CRM/common/formButtons.tpl" location="bottom"} +
+
+ +{literal} + +{/literal} diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl new file mode 100755 index 000000000..68af99c89 --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl @@ -0,0 +1,24 @@ +
+
+

+

{ts}Are you sure, you want to delete the record?{/ts}

+ +

{ts}Any existing contributions will not be deleted and will still be visible on the relevant contact records{/ts}

+
+
+ {include file="CRM/common/formButtons.tpl" location="bottom"} +
+
+ + diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl new file mode 100644 index 000000000..4fb2b9cd7 --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl @@ -0,0 +1,15 @@ +
+ {icon icon="fa-info-circle"}{/icon} + {ts}Your quotation will be emailed to the contact below as a PDF attachment.{/ts} +
+{include file="CRM/Contact/Form/Task/Email.tpl"} + +{literal} + +{/literal} \ No newline at end of file diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp new file mode 100644 index 000000000..12166036c --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp @@ -0,0 +1,3 @@ +{htxt id="sales_order_invoce_note"} +{ts}Notes will be displayed on the Quotation PDF.{/ts}

+{/htxt} diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl new file mode 100644 index 000000000..0e5b4a26c --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl @@ -0,0 +1,5 @@ +{literal} + +{/literal} diff --git a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl new file mode 100644 index 000000000..6770a4286 --- /dev/null +++ b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl @@ -0,0 +1,21 @@ + + + + + +
+
+ +{literal} + +{/literal} diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl new file mode 100644 index 000000000..4e4918993 --- /dev/null +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -0,0 +1,135 @@ + + + + + + + + + +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{ts}Client Name: {$sales_order.client.display_name}<{/ts}{ts}Date:{/ts}{$domain_name}
+ {if $sales_order.clientAddress.street_address }{$sales_order.clientAddress.street_address}{/if} + {if $sales_order.clientAddress.supplemental_address_1 }{$sales_order.clientAddress.supplemental_address_1}{/if} + {$sales_order.quotation_date|crmDate} + {if !empty($domain_location.street_address) }{$domain_location.street_address}{/if} + {if !empty($domain_location.supplemental_address_1) }{$domain_location.supplemental_address_1}{/if} +
+ {if $sales_order.clientAddress.supplemental_address_2 }{$sales_order.clientAddress.supplemental_address_2 }{/if} + {if $sales_order.clientAddress.state }{$sales_order.clientAddress.state}{/if} + Quote Number + {if !empty($domain_location.supplemental_address_2) }{$domain_location.supplemental_address_2 }{/if} + {if !empty($domain_location.state) }{$domain_location.state}{/if} +
+ {if $sales_order.clientAddress.city }{$sales_order.clientAddress.city }{/if} + {if $sales_order.clientAddress.postal_code }{$sales_order.clientAddress.postal_code}{/if} + {$sales_order.id} + {if !empty($domain_location.city) }{$domain_location.city }{/if} + {if !empty($domain_location.postal_code) }{$domain_location.postal_code}{/if} +
+ {if $sales_order.clientAddress.country }{$sales_order.clientAddress.country}{/if} + + {if !empty($domain_location.country) }{$domain_location.country }{/if} +
+ +
+ + + +
Description
{$sales_order.description}
+
+ +
+ + + + + + + + + + + + + {foreach from=$sales_order.items key=k item=item} + + + + + + + + + {/foreach} + + + + + + {foreach from=$sales_order.taxRates item=tax} + + + + + + {/foreach} + + + + + + + + + + +
{ts}Description{/ts}{ts}Quantity{/ts}{ts}Unit Price{/ts}{ts}Discount{/ts}{ts}VAT{/ts}{ts}Amount {$sales_order.currency} (without tax){/ts}
{$item.item_description}{$item.quantity}{$item.unit_price|crmMoney:$sales_order.currency}{if empty($item.discounted_percentage) } 0 {else}{$item.discounted_percentage}{/if}%{if empty($item.tax_rate) } 0 {else}{$item.tax_rate}{/if}%{$item.subtotal_amount|crmMoney:$sales_order.currency}
{ts}SubTotal (inclusive of discount){/ts}{$sales_order.total_before_tax|crmMoney:$sales_order.currency}
{ts}Total VAT ({$tax.rate}%){/ts}{$tax.value|crmMoney:$sales_order.currency}

{ts}Total Amount{/ts}{$sales_order.total_after_tax|crmMoney:$sales_order.currency}
+
+ + {if !empty($terms) } +
+ + + +
Terms
{$terms}
+
+ {/if} +
+ + + diff --git a/templates/CRM/Civicase/Page/CaseSalesOrder.tpl b/templates/CRM/Civicase/Page/CaseSalesOrder.tpl new file mode 100644 index 000000000..fe97dd9a5 --- /dev/null +++ b/templates/CRM/Civicase/Page/CaseSalesOrder.tpl @@ -0,0 +1,24 @@ +
+
+ +
+
+ diff --git a/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl b/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl new file mode 100644 index 000000000..b7ec9b85e --- /dev/null +++ b/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl @@ -0,0 +1,16 @@ +
+
+ +
+
+ diff --git a/templates/CRM/Civicase/SalesOrderCtrl.hlp b/templates/CRM/Civicase/SalesOrderCtrl.hlp new file mode 100644 index 000000000..6cd80b07d --- /dev/null +++ b/templates/CRM/Civicase/SalesOrderCtrl.hlp @@ -0,0 +1,11 @@ +{htxt id="sales_order_notes"} +

+ {ts}Add any notes related to the quotation here. They will be visible on the quotation PDF.{/ts} +

+{/htxt} + +{htxt id="sales_order_description} +

+ {ts}This is an internal field for adding a description for the quotation. It will not be displayed anywhere.{/ts} +

+{/htxt} diff --git a/tests/phpunit/BaseHeadlessTest.php b/tests/phpunit/BaseHeadlessTest.php index 2eda63621..d3ad2d3bd 100644 --- a/tests/phpunit/BaseHeadlessTest.php +++ b/tests/phpunit/BaseHeadlessTest.php @@ -3,11 +3,12 @@ use Civi\Test; use Civi\Test\HeadlessInterface; use Civi\Test\TransactionalInterface; +use PHPUnit\Framework\TestCase; /** * Base test class. */ -abstract class BaseHeadlessTest extends PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface { +abstract class BaseHeadlessTest extends TestCase implements HeadlessInterface, TransactionalInterface { /** * {@inheritDoc} diff --git a/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php b/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php index 96bf4b95f..7807aee85 100644 --- a/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php +++ b/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php @@ -1,18 +1,19 @@ registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Ensures user sees the email form when the Email URL is accessed. + */ + public function testEmailFormWillDisplayAsExpected() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + Email::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 2, + "email" => "junwe@mail.com", + "is_primary" => TRUE, + "on_hold" => 0, + ]) + ->execute(); + + $link = "civicrm/case-features/quotations/email?id={$salesOrder->id}"; + $page = $this->imitateLinkVisit($link); + + $this->assertRegExp('/name="subject"/', $page); + $this->assertRegExp('/name="from_email_address"/', $page); + } + + /** + * Ensures the To Email is set to client email on email form. + */ + public function testToEmailIsSetByDefaulToSalesOrderClientEmail() { + $expectedToEmail = "junwe@mail.com"; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + Email::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 2, + "email" => $expectedToEmail, + "is_primary" => TRUE, + "on_hold" => 0, + ]) + ->execute(); + + $link = "civicrm/case-features/quotations/email?id={$salesOrder->id}"; + $page = $this->imitateLinkVisit($link); + + $this->assertRegExp('/<' . $expectedToEmail . '>/', $page); + } + + /** + * Ensures invoice will render expected tokens & tplParams. + * + * We only cheeck for some fields, that is enough to show that + * the right case sales order entity value is passed to + * the invoice. + */ + public function testInvoiceRendersAsExpected() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $lineItem1 = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $lineItem2 = $this->getCaseSalesOrderLineData(); + + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + $address = Address::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 5, + "is_primary" => TRUE, + "is_billing" => TRUE, + "street_address" => "Coldharbour Ln", + "street_number" => "42", + "supplemental_address_1" => "Supplementary Address 1", + "supplemental_address_2" => "Supplementary Address 2", + "supplemental_address_3" => "Supplementary Address 3", + "city" => "Hayes", + "postal_code" => "UB3 3EA", + "country_id" => 1226, + "manual_geo_code" => FALSE, + "timezone" => NULL, + "name" => NULL, + "master_id" => NULL, + ]) + ->execute() + ->first(); + + $contact = (object) (Contact::get(FALSE) + ->addWhere('id', '=', $salesOrder->client_id) + ->execute() + ->first()); + + $_REQUEST['id'] = $_GET['id'] = $salesOrder->id; + + $invoice = CRM_Civicase_Form_CaseSalesOrderInvoice::getQuotationInvoice(); + + $totalBeforeTax = CRM_Utils_Money::format($salesOrder->total_before_tax, $salesOrder->currency); + $totalAfterTax = CRM_Utils_Money::format($salesOrder->total_after_tax, $salesOrder->currency); + $this->assertArrayHasKey("html", $invoice); + $this->assertRegExp('/' . $contact->display_name . '/', $invoice['html']); + $this->assertRegExp('/Supplementary Address 1/', $invoice['html']); + $this->assertRegExp('/Supplementary Address 2/', $invoice['html']); + $this->assertRegExp('/' . $salesOrder->description . '/', $invoice['html']); + $this->assertRegExp('/' . str_replace(' ', '', $totalBeforeTax) . '/', $invoice['html']); + $this->assertRegExp('/' . str_replace(' ', '', $totalAfterTax) . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem1['item_description'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem2['item_description'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem1['quantity'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem2['quantity'] . '/', $invoice['html']); + } + + /** + * Visits a CiviCRM link and returns the page content. + * + * @param string $url + * URL to the page. + * + * @return string + * Content of the page. + */ + public function imitateLinkVisit(string $url) { + $_SERVER['REQUEST_URI'] = $url; + $urlParts = explode('?', $url); + $_GET['q'] = $urlParts[0]; + + if (!empty($urlParts[1])) { + $parsed = []; + parse_str($urlParts[1], $parsed); + foreach ($parsed as $param => $value) { + $_REQUEST[$param] = $value; + } + } + + $item = CRM_Core_Invoke::getItem([$_GET['q']]); + ob_start(); + CRM_Core_Invoke::runItem($item); + return ob_get_clean(); + } + +} diff --git a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php new file mode 100644 index 000000000..ba092253e --- /dev/null +++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php @@ -0,0 +1,121 @@ +generatePriceField(); + } + + /** + * Ensures the correct number of line item is generated. + * + * When there's no previous contribution. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithoutPreviousContribution() { + $salesOrder = $this->createCaseSalesOrder(); + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_PERCENT, 25); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(2, $lineItems); + } + + /** + * Ensures the correct number of line item is generated. + * + * When there's previous contribution. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution() { + $salesOrder = $this->createCaseSalesOrder(); + + $previousContributionCount = rand(1, 4); + for ($i = 0; $i < $previousContributionCount; $i++) { + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds([$salesOrder['id']]) + ->setStatusId(1) + ->setToBeInvoiced(SalesOrderService::INVOICE_PERCENT) + ->setPercentValue(20) + ->setDate(date('Y-m-d')) + ->setFinancialTypeId('1') + ->execute(); + } + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(($previousContributionCount * 2) + 2, $lineItems); + } + + /** + * Ensures the correct number of line item is generated. + * + * When there's discount with the right value. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithDiscount() { + $percent = 20; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(['discounted_percentage' => $percent]); + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + usort($lineItems, fn($a, $b) => $a['line_total'] <=> $b['line_total']); + + $this->assertCount(2, $lineItems); + $this->assertEquals(-1 * $salesOrder['items'][0]['subtotal_amount'] * $percent / 100, $lineItems[0]['line_total']); + } + + /** + * Ensures the correct number of line item is generated. + * + * When the discount value is zero and the value is as expected. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWhenDiscountIsZero() { + $percent = 0; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(['discounted_percentage' => $percent]); + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(1, $lineItems); + $this->assertEquals($salesOrder['items'][0]['subtotal_amount'], $lineItems[0]['line_total']); + } + + /** + * Ensures the value of the generated line item is correct. + */ + public function testGeneratedPercentLineItemHasTheAppropraiteValue() { + $percent = rand(20, 40); + $salesOrder = $this->createCaseSalesOrder(); + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_PERCENT, $percent); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(2, $lineItems); + $this->assertEquals($salesOrder['items'][0]['subtotal_amount'] * $percent / 100, $lineItems[0]['line_total']); + $this->assertEquals($salesOrder['items'][1]['subtotal_amount'] * $percent / 100, $lineItems[1]['line_total']); + } + +} diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php new file mode 100644 index 000000000..441c7d12b --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php @@ -0,0 +1,114 @@ +registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Test case sales order compute action returns expected fields. + */ + public function testComputeTotalActionReturnsExpectedFields() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertArrayHasKey('taxRates', $computedTotal); + $this->assertArrayHasKey('totalBeforeTax', $computedTotal); + $this->assertArrayHasKey('totalAfterTax', $computedTotal); + } + + /** + * Test case sales order total is calculated appropraitely. + */ + public function testComputeTotalActionReturnsExpectedTotal() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertEquals($computedTotal['totalBeforeTax'], 150); + $this->assertEquals($computedTotal['totalAfterTax'], 160); + } + + /** + * Test case sales order tax rates is computed as epxected. + */ + public function testComputeTotalActionReturnsExpectedTaxRates() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10, 'tax_rate' => 2] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertNotEmpty($computedTotal['taxRates']); + $this->assertCount(2, $computedTotal['taxRates']); + + // Ensure the tax rates are sorted in ascending order of rate. + $this->assertEquals($computedTotal['taxRates'][0]['rate'], 2); + $this->assertEquals($computedTotal['taxRates'][0]['value'], 1); + $this->assertEquals($computedTotal['taxRates'][1]['rate'], 10); + $this->assertEquals($computedTotal['taxRates'][1]['value'], 10); + } + + /** + * Test compute action doesn't throw error for empty line items. + */ + public function testComputeTotalActionReturnsEmptyResultForEmptyLineItems() { + $items = []; + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertArrayHasKey('taxRates', $computedTotal); + $this->assertArrayHasKey('totalBeforeTax', $computedTotal); + $this->assertArrayHasKey('totalAfterTax', $computedTotal); + + $this->assertEmpty($computedTotal['taxRates']); + $this->assertEmpty($computedTotal['totalAfterTax']); + $this->assertEmpty($computedTotal['totalBeforeTax']); + } + +} diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php new file mode 100644 index 000000000..41a8d5351 --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php @@ -0,0 +1,473 @@ +generatePriceField(); + $contact = ContactFabricator::fabricate(); + $this->registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Ensures contribution create action updates status successfully. + */ + public function testContributionCreateActionWillUpdateSalesOrderStatus() { + $ids = []; + + for ($i = 0; $i < rand(5, 11); $i++) { + $ids[] = $this->createCaseSalesOrder()['id']; + } + + $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds($ids) + ->setStatusId($newStatus) + ->setToBeInvoiced('percent') + ->setPercentValue('100') + ->setDate('2020-2-12') + ->setFinancialTypeId('1') + ->execute(); + + $results = CaseSalesOrder::get() + ->addWhere('id', 'IN', $ids) + ->execute(); + + $iterator = $results->getIterator(); + while ($iterator->valid()) { + $this->assertEquals($newStatus, $iterator->current()['status_id']); + $iterator->next(); + } + } + + /** + * Ensures contribution create action will update contribution custom field. + */ + public function testContributionCreateActionWillUpdateCustomFields() { + $ids = []; + + for ($i = 0; $i < rand(5, 11); $i++) { + $ids[] = $this->createCaseSalesOrder()['id']; + } + + $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds($ids) + ->setStatusId($newStatus) + ->setToBeInvoiced('percent') + ->setPercentValue('100') + ->setDate('2020-2-12') + ->setFinancialTypeId('1') + ->execute(); + + $contributions = Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Quotation', 'IN', $ids) + ->execute() + ->jsonSerialize(); + + $this->assertCount(count($ids), $contributions); + $this->assertEmpty(array_diff(array_column($contributions, 'Opportunity_Details.Quotation'), $ids)); + } + + /** + * Ensures The total amount of contribution will be the expected amount. + * + * @dataProvider provideContributionCreateData + */ + public function testAppropriateContributionAmountIsCreated($expectedPercent, $contributionCreateData, $withDiscount = FALSE, $withTax = FALSE) { + $params = []; + + if ($withDiscount) { + $params['items']['discounted_percentage'] = rand(1, 50); + } + + if ($withTax) { + $params['items']['tax_rate'] = rand(1, 50); + } + + $salesOrder = $this->createCaseSalesOrder($params); + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($salesOrder['items']) + ->execute() + ->jsonSerialize()[0]; + + foreach ($contributionCreateData as $data) { + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds([$salesOrder['id']]) + ->setStatusId($data['statusId']) + ->setToBeInvoiced($data['toBeInvoiced']) + ->setPercentValue($data['percentValue']) + ->setDate($data['date']) + ->setFinancialTypeId($data['financialTypeId']) + ->execute(); + } + + $contributionAmounts = Contribution::get() + ->addSelect('total_amount') + ->addWhere('Opportunity_Details.Quotation', '=', $salesOrder['id']) + ->execute() + ->jsonSerialize(); + + $paidTotal = array_sum(array_column($contributionAmounts, 'total_amount')); + + // We can only guarantee that the value will be equal to 1 decimal place. + $this->assertEquals(round(($expectedPercent * $computedTotal['totalAfterTax']) / 100, 1), round($paidTotal, 1)); + } + + /** + * Ensures The expected numbers of contributions are created for bulk action. + * + * @dataProvider provideBulkContributionCreateData + */ + public function testExpectedContributionCountIsCreated($expectedCount, $contributionCreateData, $salesOrderData) { + $salesOrders = []; + + foreach ($salesOrderData as $data) { + $salesOrder = $this->createCaseSalesOrder(); + + if ($data['previouslyInvoiced'] > 0) { + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds([$salesOrder['id']]) + ->setStatusId(1) + ->setToBeInvoiced(CaseSalesOrderContribution::INVOICE_PERCENT) + ->setPercentValue($data['previouslyInvoiced']) + ->setDate(date("Y-m-d")) + ->setFinancialTypeId(1) + ->execute(); + } + + $salesOrders[] = $salesOrder; + } + + $result = CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds(array_column($salesOrders, 'id')) + ->setStatusId($contributionCreateData['statusId']) + ->setToBeInvoiced($contributionCreateData['toBeInvoiced']) + ->setPercentValue($contributionCreateData['percentValue']) + ->setDate($contributionCreateData['date']) + ->setFinancialTypeId($contributionCreateData['financialTypeId']) + ->execute(); + + $this->assertEquals($expectedCount, $result['created_contributions_count']); + } + + /** + * Provides data to test contribution create action. + * + * @return array + * Array of different scenarios + */ + public function provideContributionCreateData(): array { + return [ + '100% value will be total of the sales order value' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value with discount applied' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withDiscount' => TRUE, + ], + '100% value will be total of the sales order value with tax_rate applied' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withTax' => TRUE, + ], + '100% value will be total of the sales order value with tax_rate applied and paid twice' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 50, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 50, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withTax' => TRUE, + ], + '100% value will be total of the sales order value when paid in 4 instalment of 25%' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '75% value will be total of the sales order value when paid in 3 instalment of 25%' => [ + 'expectedPercent' => 75, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid at once using remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + // Expects this value to be ignored. + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid with 25% and remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid with 30%, 30% and remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 30, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 30, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + ]; + } + + /** + * Provides data to test bulk contribution create action. + * + * @return array + * Array of different scenarios + */ + public function provideBulkContributionCreateData(): array { + return [ + '1 percentvalue contribution is created for 1 quotaition' => [ + 'expectedCount' => 1, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 60, + ], + ], + ], + '2 percentvalue contributions are created for 2 quotations' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 60, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '2 percentvalues contribution are created for 2 quotations with 100% already invoiced' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 100, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + 'No remain value contribution is created for 2 quotaions with 100% already invoiced' => [ + 'expectedCount' => 0, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 100, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '1 remain value contribution is created for 2 quotaions, where only one has remain value' => [ + 'expectedCount' => 1, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 80, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '2 remain value contribution is created for 2 quotaions, where the two has remain value' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 0, + ], + [ + 'previouslyInvoiced' => 0, + ], + ], + ], + ]; + } + +} diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php new file mode 100644 index 000000000..6244d58ed --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php @@ -0,0 +1,192 @@ +registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Test case sales order and line item can be saved with the save action. + */ + public function testCanSaveCaseSalesOrder() { + $salesOrder = $this->getCaseSalesOrderData(); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $results = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertNotEmpty($results); + foreach (['client_id', 'owner_id', 'notes', 'total_before_tax'] as $key) { + $this->assertEquals($salesOrder[$key], $results[0][$key]); + } + } + + /** + * Test case sales order and line item can be saved with the save action. + */ + public function testCanSaveCaseSalesOrderAndLineItems() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $results = CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertCount(2, $results); + foreach ($results as $result) { + $this->assertEquals($result['sales_order_id'], $salesOrderId); + } + } + + /** + * Test case sales order total is calculated appropraitely. + */ + public function testSaveCaseSalesOrderTotalIsCorrect() { + $salesOrderData = $this->getCaseSalesOrderData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->jsonSerialize()[0]; + + $this->assertEquals($salesOrder['total_before_tax'], 150); + $this->assertEquals($salesOrder['total_after_tax'], 160); + } + + /** + * Test Case Sales Order save action updates sales order as expected. + */ + public function testCaseSalesOrderIsUpdatedWithSaveAction() { + $salesOrderData = $this->getCaseSalesOrderData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData(); + + // Create sales order. + $salesOrder = CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]; + + // Update Sales order. + $salesOrderData['id'] = $salesOrder['id']; + $salesOrderData['items'] = $salesOrder['items']; + $salesOrderData['notes'] = substr(md5(mt_rand()), 0, 7); + $salesOrderData['description'] = substr(md5(mt_rand()), 0, 7); + CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]; + + // Assert that sales order was updated. + $updatedSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrder['id']) + ->execute() + ->jsonSerialize()[0]; + + $this->assertEquals($salesOrderData['id'], $updatedSalesOrder['id']); + $this->assertEquals($salesOrderData['notes'], $updatedSalesOrder['notes']); + $this->assertEquals($salesOrderData['description'], $updatedSalesOrder['description']); + } + + /** + * Test line items not included in case sales order update data are removed. + */ + public function testDetachedLineItemsAreRemovedFromSalesOrderOnUpdate() { + $salesOrderData = $this->getCaseSalesOrderData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData(); + + $salesOrder = CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]; + + $this->assertCount(2, $salesOrder['items']); + + // Perform update with single line item. + $salesOrderData['id'] = $salesOrder['id']; + $salesOrderData['items'] = [$salesOrder['items'][0]]; + CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]; + + // Assert that sales order has only one line item. + $salesOrderLine = CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', $salesOrder['id']) + ->execute() + ->jsonSerialize(); + + $this->assertCount(1, $salesOrderLine); + $this->assertEquals($salesOrder['items'][0]['id'], $salesOrderLine[0]['id']); + } + + /** + * Test line item subtotal doesnt exceed product maximum cost. + */ + public function testSubTotalDoesntExceedProductCost() { + $productPrice = 200; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData( + ['unit_price' => 200, 'quantity' => 2] + ); + + $salesOrder['items'][0]['product_id'] = ProductFabricator::fabricate(['cost' => 200])['id']; + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderLine = CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertNotEmpty($salesOrderLine); + $this->assertEquals($productPrice, $salesOrderLine[0]['subtotal_amount']); + } + +} diff --git a/tests/phpunit/Helpers/CaseSalesOrderTrait.php b/tests/phpunit/Helpers/CaseSalesOrderTrait.php new file mode 100644 index 000000000..355d91363 --- /dev/null +++ b/tests/phpunit/Helpers/CaseSalesOrderTrait.php @@ -0,0 +1,151 @@ +addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute(); + + return $salesOrderStatus; + } + + /** + * Returns list of available statuses. + * + * @return array + * Array of sales order invoicing statuses + */ + public function getCaseSalesOrderInvoicingStatus() { + return OptionValue::get() + ->addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_invoicing_status') + ->execute() + ->getArrayCopy(); + } + + /** + * Returns list of available statuses. + * + * @return array + * Array of sales order payment statuses + */ + public function getCaseSalesOrderPaymentStatus() { + return OptionValue::get() + ->addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_payment_status') + ->execute() + ->getArrayCopy(); + } + + /** + * Returns fabricated case sales order data. + * + * @param array $default + * Default value. + * + * @return array + * Key-Value pair of a case sales order fields and values + */ + public function getCaseSalesOrderData(array $default = []) { + $client = ContactFabricator::fabricate(); + $caseType = CaseTypeFabricator::fabricate(); + $case = CaseFabricator::fabricate( + [ + 'case_type_id' => $caseType['id'], + 'contact_id' => $client['id'], + 'creator_id' => $client['id'], + ] + ); + + return array_merge([ + 'client_id' => $client['id'], + 'owner_id' => $client['id'], + 'case_id' => $case['id'], + 'currency' => 'GBP', + 'status_id' => $this->getCaseSalesOrderStatus()[0]['value'], + 'invoicing_status_id' => $this->getCaseSalesOrderInvoicingStatus()[0]['value'], + 'payment_status_id' => $this->getCaseSalesOrderPaymentStatus()[0]['value'], + 'description' => 'test', + 'notes' => 'test', + 'total_before_tax' => 0, + 'total_after_tax' => 0, + 'quotation_date' => '2022-08-09', + 'items' => [], + ], $default); + } + + /** + * Returns fabricated case sales order line data. + * + * @param array $default + * Default value. + * + * @return array + * Key-Value pair of a case sales order line item fields and values + */ + public function getCaseSalesOrderLineData(array $default = []) { + $product = ProductFabricator::fabricate(); + $quantity = rand(2, 9); + $unitPrice = rand(50, 1000); + + return array_merge([ + 'financial_type_id' => 1, + 'product_id' => $product['id'], + 'item_description' => 'test', + 'quantity' => $quantity, + 'unit_price' => $unitPrice, + 'tax_rate' => NULL, + 'discounted_percentage' => NULL, + 'subtotal_amount' => $quantity * $unitPrice, + ], $default); + } + + /** + * Creates case sales order. + * + * @param array $params + * Extra paramters. + * + * @return array + * Created case sales order + */ + public function createCaseSalesOrder(array $params = []): array { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + + if (!empty($params['items']['discounted_percentage'])) { + $salesOrder['items'][0]['discounted_percentage'] = $params['items']['discounted_percentage']; + } + + if (!empty($params['items']['tax_rate'])) { + $salesOrder['items'][0]['tax_rate'] = $params['items']['tax_rate']; + } + + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + return $salesOrder; + } + +} diff --git a/tests/phpunit/Helpers/PriceFieldTrait.php b/tests/phpunit/Helpers/PriceFieldTrait.php new file mode 100644 index 000000000..07c1ab481 --- /dev/null +++ b/tests/phpunit/Helpers/PriceFieldTrait.php @@ -0,0 +1,62 @@ + civicrm_api3('PriceSet', 'getvalue', [ + 'name' => 'default_contribution_amount', + 'return' => 'id', + ]), + 'options' => ['limit' => 1], + ] + ); + $priceFieldParams = $priceField; + unset( + $priceFieldParams['id'], + $priceFieldParams['name'], + $priceFieldParams['weight'], + $priceFieldParams['is_required'] + ); + $priceFieldValue = civicrm_api3('PriceFieldValue', + 'getsingle', + [ + 'price_field_id' => $priceField['id'], + 'options' => ['limit' => 1], + ] + ); + $priceFieldValueParams = $priceFieldValue; + unset($priceFieldValueParams['id'], $priceFieldValueParams['name'], $priceFieldValueParams['weight']); + for ($i = 1; $i <= 30; ++$i) { + $params = array_merge($priceFieldParams, ['label' => ts('Additional Line Item') . " $i"]); + $priceField = civicrm_api3('PriceField', 'get', $params)['values']; + if (empty($priceField)) { + $p = civicrm_api3('PriceField', 'create', $params); + civicrm_api3('PriceFieldValue', 'create', array_merge( + $priceFieldValueParams, + [ + 'label' => ts('Additional Item') . " $i", + 'price_field_id' => $p['id'], + ] + )); + } + else { + civicrm_api3('PriceField', 'create', [ + 'id' => key($priceField), + 'is_active' => TRUE, + ]); + } + } + } + +} diff --git a/tests/phpunit/api/v3/Case/BaseTestCase.php b/tests/phpunit/api/v3/Case/BaseTestCase.php index 3082b0008..e23477d2a 100644 --- a/tests/phpunit/api/v3/Case/BaseTestCase.php +++ b/tests/phpunit/api/v3/Case/BaseTestCase.php @@ -3,7 +3,7 @@ /** * Test the "Case.getfiles" API. */ -class api_v3_Case_BaseTestCase extends \PHPUnit_Framework_TestCase { +class api_v3_Case_BaseTestCase extends \PHPUnit\Framework\TestCase { protected $_apiversion = 3; protected static $filePrefix = NULL; diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index 7c194eb52..3776d45af 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -28,6 +28,41 @@ CRM_Civicase_Page_WorkflowAngular administer CiviCase + + civicrm/case-features/a + CRM_Civicase_Page_CaseFeaturesAngular + administer CiviCase + + + civicrm/case-features/quotations/view + CRM_Civicase_Page_CaseSalesOrder + administer CiviCase + + + civicrm/case-features/quotations/delete + CRM_Civicase_Form_CaseSalesOrderDelete + administer CiviCase + + + civicrm/case-features/quotations/contact-tab + CRM_Civicase_Page_ContactCaseSalesOrderTab + administer CiviCase + + + civicrm/case-features/quotations/create-contribution + CRM_Civicase_Form_CaseSalesOrderContributionCreate + administer CiviCase + + + civicrm/case-features/quotations/download-pdf + CRM_Civicase_Form_CaseSalesOrderInvoice::download + administer CiviCase + + + civicrm/case-features/quotations/email + CRM_Civicase_Form_CaseSalesOrderInvoice + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms diff --git a/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php b/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php new file mode 100644 index 000000000..c9030d554 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php @@ -0,0 +1,17 @@ + 'CaseCategoryFeatures', + 'class' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'table' => 'civicrm_case_category_features', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml b/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml new file mode 100644 index 000000000..80e13d60a --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml @@ -0,0 +1,43 @@ + + + + CRM/Civicase + CaseCategoryFeatures + civicrm_case_category_features + Stores additional features enabled for a case category + true + + + id + int unsigned + true + Unique CaseCategoryFeatures ID + + Number + + + + id + true + + + + category_id + int unsigned + One of the values of the case_type_categories option group + true + + case_type_categories + + + + + feature_id + int unsigned + One of the values of the case_type_category_features option group + true + + case_type_category_features + + +
diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php new file mode 100644 index 000000000..55b2d44de --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php @@ -0,0 +1,17 @@ + 'CaseSalesOrder', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'table' => 'civicase_sales_order', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml new file mode 100644 index 000000000..e7a4630bb --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -0,0 +1,205 @@ + + + + CRM/Civicase + CaseSalesOrder + civicase_sales_order + Sales order that represents quotations + true + + + civicrm/case-features/quotations/view?reset=1&id=[id] + civicrm/case-features/a#/quotations/new?reset=1&id=[id] + civicrm/case-features/quotations/delete?reset=1&id=[id] + + + + id + int unsigned + true + Unique CaseSalesOrder ID + + Number + + + + id + true + + + + client_id + int unsigned + FK to Contact + + + EntityRef + + + + client_id +
civicrm_contact
+ id + CASCADE + + + + owner_id + int unsigned + FK to Contact + + + EntityRef + + + + owner_id + civicrm_contact
+ id + CASCADE +
+ + + case_id + int unsigned + FK to Case + + + EntityRef + + + + case_id + civicrm_case
+ id + CASCADE +
+ + + currency + Financial Currency + varchar + 3 + NULL + /cur(rency)?/i + /^[A-Z]{3}$/ + 3 character string, value from config setting or input via user. + + civicrm_currency
+ name + full_name + name + symbol +
+ + Select + +
+ + + status_id + int unsigned + One of the values of the case_sales_order_status option group + true + + case_sales_order_status + + + + Select + + + + + invoicing_status_id + int unsigned + One of the values of the case_sales_order_invoicing_status option group + true + + case_sales_order_invoicing_status + + + + Select + + + + + payment_status_id + int unsigned + One of the values of the case_sales_order_payment_status option group + true + + case_sales_order_payment_status + + + + Select + + + + + description + text + false + Sales order deesctiption + + + TextArea + + + + + notes + text + false + Sales order notes + + + RichTextEditor + + + + + total_before_tax + decimal + false + Total amount of the sales order line items before tax deduction. + + Text + + + + + total_after_tax + decimal + false + Total amount of the sales order line items after tax deduction. + + Text + + + + + quotation_date + timestamp + Quotation date + + Select Date + + + + + created_at + timestamp + Date the sales order is created + CURRENT_TIMESTAMP + + + + is_deleted + boolean + 0 + Is this sales order deleted? + + diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php new file mode 100644 index 000000000..4547d665b --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php @@ -0,0 +1,17 @@ + 'CaseSalesOrderLine', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'table' => 'civicase_sales_order_line', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml new file mode 100644 index 000000000..8a7bccdd0 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml @@ -0,0 +1,136 @@ + + + + CRM/Civicase + CaseSalesOrderLine + civicase_sales_order_line + Sales order line items + true + + + id + int unsigned + true + Unique CaseSalesOrderLine ID + + Number + + + + id + true + + + + sales_order_id + int unsigned + FK to CaseSalesOrder + + EntityRef + + + + sales_order_id +
civicase_sales_order
+ id + CASCADE + + + + financial_type_id + int unsigned + FK to CiviCRM Financial Type + + + EntityRef + + + + civicrm_financial_type
+ id + name +
+ + financial_type_id + civicrm_financial_type
+ id + SET NULL +
+ + + product_id + Product ID + int unsigned + + + EntityRef + + + + product_id + civicrm_product
+ id + SET NULL +
+ + + item_description + text + false + line item deesctiption + + + TextArea + + + + + quantity + decimal + Quantity + + + Text + + + + + unit_price + decimal + Unit Price + + + Text + + + + + tax_rate + decimal + Tax rate for the line item + + + Text + + + + + discounted_percentage + decimal + Discount applied to the line item + + + Text + + + + + subtotal_amount + decimal + Quantity x Unit Price x (100-Discount)% + + + Text + + +