Plugin Directory

Changeset 3488606


Ignore:
Timestamp:
03/23/2026 06:39:37 AM (5 days ago)
Author:
wprashed
Message:

2.1.0

  • Added a full visual rule builder to the settings page
  • Moved the plugin page under the WooCommerce admin menu
  • Added customer type, product, and tag timeout rules
  • Added excluded role and excluded tag support
  • Expanded CSV import to support all new rule types
  • Redesigned the admin settings interface with a more modern layout
  • Improved import/export presentation and rule card usability
Location:
cartflush-autoclear-cart-for-inactive-users
Files:
29 added
5 edited

Legend:

Unmodified
Added
Removed
  • cartflush-autoclear-cart-for-inactive-users/trunk/assets/css/admin.css

    r3484352 r3488606  
    11.cartflush-admin {
    2     max-width: 1120px;
     2    --cf-bg: #f1f5f9;
     3    --cf-surface: #ffffff;
     4    --cf-surface-soft: #f8fafc;
     5    --cf-border: #e2e8f0;
     6    --cf-border-strong: #cbd5e1;
     7    --cf-text: #1e293b;
     8    --cf-text-soft: #64748b;
     9    --cf-title: #0f172a;
     10    --cf-primary: #3b82f6;
     11    --cf-primary-deep: #2563eb;
     12    --cf-primary-soft: #eff6ff;
     13    --cf-success: #0f766e;
     14    --cf-danger: #dc2626;
     15    --cf-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
     16    max-width: 1340px;
     17    padding-bottom: 32px;
     18}
     19
     20.cartflush-admin::before {
     21    content: "";
     22    position: fixed;
     23    inset: 0;
     24    z-index: -1;
     25    background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
     26    pointer-events: none;
     27}
     28
     29.cartflush-shell {
     30    display: grid;
     31    gap: 22px;
     32}
     33
     34.cartflush-hero,
     35.cartflush-panel,
     36.cartflush-sidecard,
     37.cartflush-summary-panel {
     38    border: 1px solid var(--cf-border);
     39    border-radius: 20px;
     40    background: var(--cf-surface);
     41    box-shadow: var(--cf-shadow);
    342}
    443
    544.cartflush-hero {
     45    display: grid;
     46    grid-template-columns: minmax(0, 1.8fr) minmax(280px, 0.8fr);
     47    gap: 24px;
     48    padding: 28px 30px;
     49}
     50
     51.cartflush-eyebrow,
     52.cartflush-chip {
     53    display: inline-flex;
     54    align-items: center;
     55    padding: 6px 12px;
     56    border-radius: 999px;
     57    font-size: 11px;
     58    font-weight: 700;
     59    letter-spacing: 0.08em;
     60    text-transform: uppercase;
     61}
     62
     63.cartflush-eyebrow {
     64    background: var(--cf-primary-soft);
     65    color: var(--cf-primary-deep);
     66}
     67
     68.cartflush-chip {
     69    background: #f1f5f9;
     70    color: var(--cf-text-soft);
     71}
     72
     73.cartflush-chip--blue {
     74    background: var(--cf-primary-soft);
     75    color: var(--cf-primary-deep);
     76}
     77
     78.cartflush-hero h1 {
     79    margin: 14px 0 10px;
     80    font-size: 34px;
     81    line-height: 1.08;
     82    letter-spacing: -0.03em;
     83    color: var(--cf-title);
     84}
     85
     86.cartflush-hero p,
     87.cartflush-rule-card p,
     88.cartflush-panel__intro p,
     89.cartflush-sidecard p,
     90.cartflush-savebar p,
     91.cartflush-summary p {
     92    margin-top: 0;
     93    color: var(--cf-text-soft);
     94    line-height: 1.7;
     95}
     96
     97.cartflush-hero__stats {
     98    display: grid;
     99    grid-template-columns: repeat(4, minmax(0, 1fr));
     100    gap: 14px;
     101    margin-top: 24px;
     102}
     103
     104.cartflush-stat {
     105    padding: 16px 18px;
     106    border: 1px solid var(--cf-border);
     107    border-radius: 16px;
     108    background: var(--cf-surface-soft);
     109}
     110
     111.cartflush-stat__label,
     112.cartflush-stat__meta {
     113    display: block;
     114    font-size: 12px;
     115    color: var(--cf-text-soft);
     116}
     117
     118.cartflush-stat strong {
     119    display: block;
     120    margin: 8px 0 6px;
     121    font-size: 28px;
     122    line-height: 1;
     123    letter-spacing: -0.04em;
     124    color: var(--cf-title);
     125}
     126
     127.cartflush-hero__rail {
     128    display: grid;
     129    align-content: start;
     130}
     131
     132.cartflush-rail-card {
     133    padding: 20px;
     134    border: 1px solid var(--cf-border);
     135    border-radius: 16px;
     136    background: var(--cf-surface-soft);
     137}
     138
     139.cartflush-rail-card h3 {
     140    margin: 14px 0 8px;
     141    font-size: 20px;
     142    line-height: 1.25;
     143    color: var(--cf-title);
     144}
     145
     146.cartflush-rail-card p {
     147    margin: 0;
     148}
     149
     150.cartflush-bullet-list {
     151    margin: 14px 0 0;
     152    padding-left: 18px;
     153    color: var(--cf-text-soft);
     154    line-height: 1.8;
     155}
     156
     157.cartflush-layout {
     158    display: grid;
     159    grid-template-columns: minmax(0, 1.9fr) minmax(300px, 0.9fr);
     160    gap: 22px;
     161    align-items: start;
     162}
     163
     164.cartflush-main,
     165.cartflush-sidebar {
     166    display: grid;
     167    gap: 18px;
     168}
     169
     170.cartflush-sidepanel {
     171    display: grid;
     172    gap: 16px;
     173}
     174
     175.cartflush-sidepanel__intro {
     176    padding: 8px 4px 0;
     177}
     178
     179.cartflush-sidepanel__intro h2 {
     180    margin: 10px 0 8px;
     181    font-size: 24px;
     182    line-height: 1.15;
     183    letter-spacing: -0.02em;
     184    color: var(--cf-title);
     185}
     186
     187.cartflush-sidepanel__intro p {
     188    margin: 0;
     189    color: var(--cf-text-soft);
     190    line-height: 1.7;
     191}
     192
     193.cartflush-panel,
     194.cartflush-summary-panel {
     195    padding: 28px;
     196}
     197
     198.cartflush-panel__intro {
     199    margin-bottom: 22px;
     200}
     201
     202.cartflush-panel h2,
     203.cartflush-summary-panel h2 {
     204    margin: 10px 0 8px;
     205    font-size: 24px;
     206    line-height: 1.15;
     207    letter-spacing: -0.02em;
     208    color: var(--cf-title);
     209}
     210
     211.cartflush-settings-form {
     212    display: grid;
     213    gap: 20px;
     214}
     215
     216.cartflush-settings-form .form-table {
     217    margin: 0;
     218}
     219
     220.cartflush-settings-form .form-table th {
     221    padding-left: 0;
     222    color: var(--cf-title);
     223    font-size: 14px;
     224    font-weight: 600;
     225}
     226
     227.cartflush-settings-form .form-table td {
     228    padding-right: 0;
     229}
     230
     231.cartflush-field {
     232    display: inline-flex;
     233    align-items: center;
     234    gap: 12px;
     235    padding: 12px 14px;
     236    border: 1px solid var(--cf-border);
     237    border-radius: 14px;
     238    background: var(--cf-surface-soft);
     239}
     240
     241.cartflush-field input[type="number"] {
     242    border: 0;
     243    box-shadow: none;
     244    background: transparent;
     245    font-size: 20px;
     246    font-weight: 700;
     247    color: var(--cf-title);
     248}
     249
     250.cartflush-field span {
     251    font-size: 12px;
     252    font-weight: 700;
     253    color: var(--cf-text-soft);
     254    letter-spacing: 0.08em;
     255    text-transform: uppercase;
     256}
     257
     258.cartflush-rule-grid,
     259.cartflush-summary {
     260    display: grid;
     261    grid-template-columns: repeat(2, minmax(0, 1fr));
     262    gap: 18px;
     263}
     264
     265.cartflush-rule-card {
     266    padding: 20px;
     267    border: 1px solid var(--cf-border);
     268    border-radius: 20px;
     269    background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
     270}
     271
     272.cartflush-rule-card__header {
    6273    display: flex;
    7274    justify-content: space-between;
    8275    align-items: flex-start;
    9     gap: 24px;
    10     margin: 24px 0;
    11     padding: 28px 32px;
    12     border-radius: 20px;
    13     background: linear-gradient(135deg, #0f172a, #1d4ed8);
    14     color: #fff;
    15     box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16);
    16 }
    17 
    18 .cartflush-hero h1 {
    19     margin: 0 0 10px;
    20     font-size: 30px;
    21     line-height: 1.15;
    22     color: #fff;
    23 }
    24 
    25 .cartflush-hero p {
    26     margin: 0;
    27     max-width: 720px;
    28     font-size: 14px;
    29     line-height: 1.7;
    30     color: rgba(255, 255, 255, 0.88);
    31 }
    32 
    33 .cartflush-hero__meta {
    34     display: flex;
    35     flex-wrap: wrap;
    36     gap: 10px;
    37 }
    38 
    39 .cartflush-hero__meta span {
    40     display: inline-flex;
    41     align-items: center;
    42     padding: 8px 12px;
    43     border: 1px solid rgba(255, 255, 255, 0.2);
    44     border-radius: 999px;
    45     background: rgba(255, 255, 255, 0.12);
    46     font-size: 12px;
    47     font-weight: 600;
    48     letter-spacing: 0.02em;
    49 }
    50 
    51 .cartflush-grid {
    52     display: grid;
    53     grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    54     gap: 20px;
    55     margin-bottom: 20px;
    56 }
    57 
    58 .cartflush-card {
    59     padding: 24px;
    60     border: 1px solid #d0d7de;
    61     border-radius: 18px;
    62     background: #fff;
    63     box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
    64 }
    65 
    66 .cartflush-card--primary {
    67     grid-column: span 2;
    68 }
    69 
    70 .cartflush-card--wide {
    71     margin-top: 8px;
    72 }
    73 
    74 .cartflush-card h2 {
    75     margin-top: 0;
    76     margin-bottom: 10px;
    77     font-size: 20px;
    78 }
    79 
    80 .cartflush-card h3 {
    81     margin-top: 0;
    82     margin-bottom: 8px;
    83     font-size: 15px;
    84 }
    85 
    86 .cartflush-card p {
    87     line-height: 1.65;
    88 }
    89 
    90 .cartflush-card form {
    91     margin-top: 14px;
    92 }
    93 
    94 .cartflush-card input[type="file"] {
    95     width: 100%;
    96     padding: 10px;
    97     border: 1px dashed #94a3b8;
    98     border-radius: 12px;
    99     background: #f8fafc;
    100 }
    101 
    102 .cartflush-card .button {
    103     margin-top: 14px;
    104 }
    105 
    106 .cartflush-field {
     276    gap: 14px;
     277    margin-bottom: 18px;
     278}
     279
     280.cartflush-rule-card__title {
     281    display: grid;
     282    gap: 8px;
     283}
     284
     285.cartflush-rule-card__meta {
    107286    display: inline-flex;
    108287    align-items: center;
    109288    gap: 8px;
    110 }
    111 
    112 .cartflush-summary {
    113     display: grid;
    114     grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
     289    width: fit-content;
     290    padding: 6px 10px;
     291    border: 1px solid var(--cf-border);
     292    border-radius: 999px;
     293    background: #fff;
     294}
     295
     296.cartflush-rule-card__count {
     297    font-size: 13px;
     298    font-weight: 700;
     299    color: var(--cf-title);
     300}
     301
     302.cartflush-rule-card__label {
     303    font-size: 11px;
     304    font-weight: 700;
     305    letter-spacing: 0.08em;
     306    text-transform: uppercase;
     307    color: var(--cf-text-soft);
     308}
     309
     310.cartflush-rule-card h3,
     311.cartflush-sidecard h3,
     312.cartflush-summary h3 {
     313    margin: 0 0 6px;
     314    font-size: 18px;
     315    line-height: 1.3;
     316    color: var(--cf-title);
     317}
     318
     319.cartflush-table-wrap {
     320    overflow-x: auto;
     321    padding: 2px 0;
     322}
     323
     324.cartflush-rule-table {
     325    width: 100%;
     326    border: 0;
     327    box-shadow: none;
     328    background: transparent;
     329    border-collapse: separate;
     330    border-spacing: 0 10px;
     331}
     332
     333.cartflush-rule-table thead th {
     334    padding: 0 8px 2px 8px;
     335    border: 0;
     336    background: transparent;
     337    font-size: 11px;
     338    font-weight: 700;
     339    text-transform: uppercase;
     340    letter-spacing: 0.08em;
     341    color: var(--cf-text-soft);
     342}
     343
     344.cartflush-rule-table tbody tr,
     345.cartflush-rule-table.striped > tbody > :nth-child(odd),
     346.cartflush-rule-table.striped > tbody > :nth-child(even) {
     347    background: transparent;
     348}
     349
     350.cartflush-rule-table tbody td {
     351    padding: 10px;
     352    border-top: 1px solid var(--cf-border);
     353    border-bottom: 1px solid var(--cf-border);
     354    background: #fff;
     355    vertical-align: middle;
     356}
     357
     358.cartflush-rule-table tbody td:first-child {
     359    border-left: 1px solid var(--cf-border);
     360    border-radius: 14px 0 0 14px;
     361}
     362
     363.cartflush-rule-table tbody td:last-child {
     364    border-right: 1px solid var(--cf-border);
     365    border-radius: 0 14px 14px 0;
     366}
     367
     368.cartflush-rule-table select,
     369.cartflush-rule-table input[type="number"],
     370.cartflush-sidecard input[type="file"] {
     371    width: 100%;
     372    max-width: 100%;
     373    min-height: 42px;
     374    padding: 0 14px;
     375    border: 1px solid var(--cf-border-strong);
     376    border-radius: 12px;
     377    background: #fff;
     378    color: var(--cf-text);
     379}
     380
     381.cartflush-sidecard input[type="file"] {
     382    padding: 11px 14px;
     383    background: #fff;
     384}
     385
     386.cartflush-rule-table select:focus,
     387.cartflush-rule-table input[type="number"]:focus,
     388.cartflush-sidecard input[type="file"]:focus,
     389.cartflush-action:focus,
     390.cartflush-savebar__button:focus {
     391    border-color: var(--cf-primary);
     392    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.14);
     393    outline: 0;
     394}
     395
     396.cartflush-rule-table__action,
     397.cartflush-row-action {
     398    width: 96px;
     399    text-align: right;
     400}
     401
     402.cartflush-remove-row {
     403    display: inline-flex;
     404    align-items: center;
     405    justify-content: center;
     406    min-height: 34px;
     407    padding: 0 10px;
     408    border: 1px solid rgba(220, 38, 38, 0.18);
     409    border-radius: 999px;
     410    background: #fff;
     411    color: var(--cf-danger);
     412    font-weight: 600;
     413    text-decoration: none;
     414}
     415
     416.cartflush-remove-row:hover,
     417.cartflush-remove-row:focus {
     418    border-color: rgba(220, 38, 38, 0.34);
     419    background: #fef2f2;
     420    color: #b91c1c;
     421}
     422
     423.cartflush-rule-card__hint {
     424    margin: 14px 0 0;
     425    font-size: 13px;
     426    color: var(--cf-text-soft);
     427}
     428
     429.cartflush-action,
     430.cartflush-sidecard .button,
     431.cartflush-savebar__button {
     432    border-radius: 12px;
     433    box-shadow: none;
     434}
     435
     436.cartflush-action {
     437    margin-top: 0;
     438    border-color: var(--cf-border);
     439    background: var(--cf-surface);
     440    color: var(--cf-title);
     441}
     442
     443.cartflush-action:hover,
     444.cartflush-action:focus {
     445    border-color: var(--cf-primary);
     446    background: var(--cf-primary-soft);
     447    color: var(--cf-primary-deep);
     448}
     449
     450.cartflush-savebar {
     451    display: flex;
     452    justify-content: space-between;
     453    align-items: center;
    115454    gap: 16px;
     455    padding: 18px 20px;
     456    border: 1px solid var(--cf-border);
     457    border-radius: 18px;
     458    background: var(--cf-surface-soft);
     459}
     460
     461.cartflush-savebar strong {
     462    display: block;
     463    margin-bottom: 4px;
     464    font-size: 18px;
     465    color: var(--cf-title);
     466}
     467
     468.cartflush-savebar__button {
     469    margin: 0;
     470    padding: 0 22px;
     471    border-color: var(--cf-primary);
     472    background: var(--cf-primary);
     473}
     474
     475.cartflush-savebar__button:hover,
     476.cartflush-savebar__button:focus {
     477    border-color: var(--cf-primary-deep);
     478    background: var(--cf-primary-deep);
     479}
     480
     481.cartflush-sidecard {
     482    padding: 22px;
     483    border-radius: 18px;
     484}
     485
     486.cartflush-sidecard__header {
     487    display: grid;
     488    gap: 10px;
     489    margin-bottom: 10px;
     490}
     491
     492.cartflush-sidecard__header strong {
     493    font-size: 20px;
     494    line-height: 1.25;
     495    color: var(--cf-title);
     496}
     497
     498.cartflush-sidecard form {
     499    display: grid;
     500    gap: 12px;
     501    margin-top: 14px;
     502}
     503
     504.cartflush-sidebar form + form {
     505    margin-top: 12px;
     506}
     507
     508.cartflush-upload-field {
     509    display: grid;
     510    gap: 8px;
     511    padding: 16px;
     512    border: 1px dashed var(--cf-border-strong);
     513    border-radius: 16px;
     514    background: var(--cf-surface-soft);
     515}
     516
     517.cartflush-upload-field > span {
     518    font-size: 12px;
     519    font-weight: 700;
     520    letter-spacing: 0.08em;
     521    text-transform: uppercase;
     522    color: var(--cf-text-soft);
     523}
     524
     525.cartflush-sidecard .button {
     526    margin-top: 0;
     527    justify-self: start;
     528}
     529
     530.cartflush-sidecard .button-secondary {
     531    border-color: var(--cf-primary);
     532    background: #fff;
     533    color: var(--cf-primary-deep);
     534}
     535
     536.cartflush-sidecard .button-secondary:hover,
     537.cartflush-sidecard .button-secondary:focus {
     538    border-color: var(--cf-primary-deep);
     539    background: var(--cf-primary-soft);
     540    color: var(--cf-primary-deep);
     541}
     542
     543.cartflush-code-list {
     544    display: flex;
     545    flex-wrap: wrap;
     546    gap: 8px;
     547    margin-top: 14px;
     548}
     549
     550.cartflush-code-list code {
     551    display: inline-flex;
     552    padding: 7px 10px;
     553    border: 1px solid var(--cf-border);
     554    border-radius: 999px;
     555    background: var(--cf-surface-soft);
     556    font-size: 12px;
     557    color: var(--cf-text-soft);
     558}
     559
     560.cartflush-summary-panel {
     561    padding: 24px;
    116562}
    117563
    118564.cartflush-summary > div {
    119565    padding: 18px;
    120     border-radius: 14px;
    121     background: #f8fafc;
    122     border: 1px solid #e2e8f0;
     566    border: 1px solid var(--cf-border);
     567    border-radius: 16px;
     568    background: var(--cf-surface-soft);
     569}
     570
     571.cartflush-summary h3 {
     572    font-size: 16px;
    123573}
    124574
     
    127577}
    128578
     579@media (max-width: 1180px) {
     580    .cartflush-layout {
     581        grid-template-columns: 1fr;
     582    }
     583
     584    .cartflush-hero__stats {
     585        grid-template-columns: repeat(2, minmax(0, 1fr));
     586    }
     587}
     588
    129589@media (max-width: 900px) {
    130     .cartflush-hero {
     590    .cartflush-hero,
     591    .cartflush-rule-grid,
     592    .cartflush-summary {
     593        grid-template-columns: 1fr;
     594    }
     595
     596    .cartflush-rule-card__header,
     597    .cartflush-savebar {
    131598        flex-direction: column;
    132599    }
    133600
    134     .cartflush-card--primary {
    135         grid-column: span 1;
     601    .cartflush-action,
     602    .cartflush-savebar__button,
     603    .cartflush-sidecard .button {
     604        width: 100%;
     605        justify-content: center;
    136606    }
    137 }
     607
     608    .cartflush-rule-table__action,
     609    .cartflush-row-action {
     610        width: 100%;
     611        text-align: left;
     612    }
     613}
  • cartflush-autoclear-cart-for-inactive-users/trunk/cartflush-autoclear-cart-for-inactive-users.php

    r3484352 r3488606  
    44 * Plugin URI: https://wordpress.org/plugins/cartflush-autoclear-cart-for-inactive-users/
    55 * Description: Automatically clears WooCommerce carts after inactivity with configurable default timeouts, import/export tools, and rule-based exclusions.
    6  * Version: 2.0.0
     6 * Version: 2.1.0
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
     
    2121}
    2222
    23 define( 'CARTFLUSH_VERSION', '1.2.1' );
     23define( 'CARTFLUSH_VERSION', '2.1.0' );
    2424define( 'CARTFLUSH_FILE', __FILE__ );
    2525define( 'CARTFLUSH_PATH', plugin_dir_path( __FILE__ ) );
  • cartflush-autoclear-cart-for-inactive-users/trunk/includes/admin/class-cartflush-admin.php

    r3484352 r3488606  
    1919    private $rules;
    2020
    21     /**
    22      * Constructor.
    23      *
    24      * @param CartFlush_Rules $rules Rules manager.
    25      */
    2621    public function __construct( CartFlush_Rules $rules ) {
    2722        $this->rules = $rules;
    28 
    2923        add_action( 'admin_menu', [ $this, 'add_settings_page' ] );
    3024        add_action( 'admin_init', [ $this, 'register_settings' ] );
     
    3428        add_action( 'admin_post_cartflush_import_csv', [ $this, 'handle_csv_import' ] );
    3529        add_action( 'admin_post_cartflush_export_json', [ $this, 'handle_json_export' ] );
    36     }
    37 
    38     /**
    39      * Add settings page under Settings.
    40      *
    41      * @return void
    42      */
     30        add_filter( 'option_page_capability_cartflush_settings_group', [ $this, 'settings_capability' ] );
     31    }
     32
    4333    public function add_settings_page() {
    44         add_options_page(
    45             __( 'CartFlush Settings', 'cartflush' ),
    46             __( 'CartFlush', 'cartflush' ),
    47             'manage_options',
    48             'cartflush-settings',
    49             [ $this, 'settings_page_html' ]
    50         );
    51     }
    52 
    53     /**
    54      * Register plugin settings.
    55      *
    56      * @return void
    57      */
     34        add_submenu_page( 'woocommerce', __( 'CartFlush Settings', 'cartflush' ), __( 'CartFlush', 'cartflush' ), $this->get_required_capability(), 'cartflush-settings', [ $this, 'settings_page_html' ] );
     35    }
     36
    5837    public function register_settings() {
    59         register_setting(
    60             'cartflush_settings_group',
    61             'cartflush_expiration_time',
    62             [
    63                 'type'              => 'integer',
    64                 'sanitize_callback' => 'absint',
    65                 'default'           => 30,
    66             ]
    67         );
    68 
    69         register_setting(
    70             'cartflush_settings_group',
    71             CartFlush_Rules::OPTION_NAME,
    72             [
    73                 'type'              => 'array',
    74                 'sanitize_callback' => [ $this, 'sanitize_rules_option' ],
    75                 'default'           => $this->rules->get_default_rules(),
    76             ]
    77         );
    78 
    79         add_settings_section(
    80             'cartflush_main',
    81             __( 'Timeout Settings', 'cartflush' ),
    82             [ $this, 'render_main_section' ],
    83             'cartflush-settings'
    84         );
    85 
    86         add_settings_field(
    87             'cartflush_expiration_time',
    88             __( 'Default cart expiration', 'cartflush' ),
    89             [ $this, 'render_expiration_field' ],
    90             'cartflush-settings',
    91             'cartflush_main'
    92         );
    93     }
    94 
    95     /**
    96      * Sanitize imported rules.
    97      *
    98      * @param mixed $value Rules payload.
    99      * @return array<string, mixed>
    100      */
     38        register_setting( 'cartflush_settings_group', 'cartflush_expiration_time', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 30 ] );
     39        register_setting( 'cartflush_settings_group', CartFlush_Rules::OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_rules_option' ], 'default' => $this->rules->get_default_rules() ] );
     40        add_settings_section( 'cartflush_main', __( 'Timeout Settings', 'cartflush' ), [ $this, 'render_main_section' ], 'cartflush-settings' );
     41        add_settings_field( 'cartflush_expiration_time', __( 'Default cart expiration', 'cartflush' ), [ $this, 'render_expiration_field' ], 'cartflush-settings', 'cartflush_main' );
     42    }
     43
    10144    public function sanitize_rules_option( $value ) {
    102         return $this->rules->normalize_rules_data( $value );
    103     }
    104 
    105     /**
    106      * Enqueue admin assets on the plugin settings page.
    107      *
    108      * @param string $hook Current admin hook suffix.
    109      * @return void
    110      */
     45        return $this->rules->normalize_rules_data( is_array( $value ) ? $this->prepare_rules_for_storage( $value ) : $value );
     46    }
     47
     48    public function settings_capability() {
     49        return $this->get_required_capability();
     50    }
     51
    11152    public function enqueue_assets( $hook ) {
    112         if ( 'settings_page_cartflush-settings' !== $hook ) {
     53        if ( 'woocommerce_page_cartflush-settings' !== $hook ) {
    11354            return;
    11455        }
    11556
    116         wp_enqueue_style(
    117             'cartflush-admin',
    118             CARTFLUSH_URL . 'assets/css/admin.css',
    119             [],
    120             CARTFLUSH_VERSION
    121         );
    122     }
    123 
    124     /**
    125      * Render settings section intro.
    126      *
    127      * @return void
    128      */
     57        wp_enqueue_style( 'cartflush-admin', CARTFLUSH_URL . 'assets/css/admin.css', [], CARTFLUSH_VERSION );
     58        wp_enqueue_script( 'cartflush-admin', CARTFLUSH_URL . 'assets/js/admin.js', [], CARTFLUSH_VERSION, true );
     59    }
     60
    12961    public function render_main_section() {
    130         echo '<p>' . esc_html__( 'Set a default inactivity window. Imported role and category rules can override it for specific carts.', 'cartflush' ) . '</p>';
    131     }
    132 
    133     /**
    134      * Render expiration input.
    135      *
    136      * @return void
    137      */
     62        echo '<p>' . esc_html__( 'Set the fallback timeout first, then add rule-based overrides and exclusions below.', 'cartflush' ) . '</p>';
     63    }
     64
    13865    public function render_expiration_field() {
    13966        $value = (int) get_option( 'cartflush_expiration_time', 30 );
    140         echo '<label class="cartflush-field">';
    141         echo '<input type="number" min="1" step="1" class="small-text" name="cartflush_expiration_time" value="' . esc_attr( $value ) . '"> ';
    142         echo '<span>' . esc_html__( 'minutes', 'cartflush' ) . '</span>';
    143         echo '</label>';
    144         echo '<p class="description">' . esc_html__( 'This fallback timeout is used when no imported rule matches the current customer or cart contents.', 'cartflush' ) . '</p>';
    145     }
    146 
    147     /**
    148      * Output admin notices.
    149      *
    150      * @return void
    151      */
     67        echo '<label class="cartflush-field"><input type="number" min="1" step="1" class="small-text" name="cartflush_expiration_time" value="' . esc_attr( $value ) . '"> <span>' . esc_html__( 'minutes', 'cartflush' ) . '</span></label>';
     68        echo '<p class="description">' . esc_html__( 'Fallback timeout when no custom timeout rule matches.', 'cartflush' ) . '</p>';
     69    }
     70
    15271    public function render_admin_notices() {
    15372        if ( ! isset( $_GET['page'] ) || 'cartflush-settings' !== sanitize_key( wp_unslash( $_GET['page'] ) ) ) {
    15473            return;
    15574        }
    156 
     75        if ( ! empty( $_GET['settings-updated'] ) ) {
     76            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'cartflush' ) . '</p></div>';
     77        }
    15778        if ( ! empty( $_GET['cartflush_notice'] ) ) {
    15879            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( wp_unslash( $_GET['cartflush_notice'] ) ) . '</p></div>';
    15980        }
    160 
    16181        if ( ! empty( $_GET['cartflush_error'] ) ) {
    16282            echo '<div class="notice notice-error"><p>' . esc_html( wp_unslash( $_GET['cartflush_error'] ) ) . '</p></div>';
     
    16484    }
    16585
    166     /**
    167      * Import a full JSON settings file.
    168      *
    169      * @return void
    170      */
    17186    public function handle_json_import() {
    17287        $this->assert_admin_permissions();
    17388        check_admin_referer( 'cartflush_import_json' );
    174 
    17589        if ( empty( $_FILES['cartflush_json_file']['tmp_name'] ) ) {
    17690            $this->redirect_with_message( '', __( 'Please choose a JSON file to import.', 'cartflush' ) );
    17791        }
    178 
    179         $raw = file_get_contents( $_FILES['cartflush_json_file']['tmp_name'] ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
     92        $raw  = file_get_contents( $_FILES['cartflush_json_file']['tmp_name'] ); // phpcs:ignore
    18093        $data = json_decode( $raw, true );
    181 
    18294        if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
    18395            $this->redirect_with_message( '', __( 'The uploaded JSON file is invalid.', 'cartflush' ) );
    18496        }
    185 
    18697        $default_timeout = isset( $data['cartflush_expiration_time'] ) ? absint( $data['cartflush_expiration_time'] ) : get_option( 'cartflush_expiration_time', 30 );
    18798        $rules           = isset( $data['import_rules'] ) ? $data['import_rules'] : $data;
    188         $normalized      = $this->rules->normalize_rules_data( $rules );
    189 
    19099        update_option( 'cartflush_expiration_time', max( 1, $default_timeout ) );
    191         update_option( CartFlush_Rules::OPTION_NAME, $normalized );
    192 
     100        update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) );
    193101        $this->redirect_with_message( __( 'JSON settings imported successfully.', 'cartflush' ) );
    194102    }
    195103
    196     /**
    197      * Import timeout and exclusion rules from CSV.
    198      *
    199      * @return void
    200      */
    201104    public function handle_csv_import() {
    202105        $this->assert_admin_permissions();
    203106        check_admin_referer( 'cartflush_import_csv' );
    204 
    205107        if ( empty( $_FILES['cartflush_csv_file']['tmp_name'] ) ) {
    206108            $this->redirect_with_message( '', __( 'Please choose a CSV file to import.', 'cartflush' ) );
    207109        }
    208 
    209         $handle = fopen( $_FILES['cartflush_csv_file']['tmp_name'], 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
    210 
     110        $handle = fopen( $_FILES['cartflush_csv_file']['tmp_name'], 'r' ); // phpcs:ignore
    211111        if ( ! $handle ) {
    212112            $this->redirect_with_message( '', __( 'The uploaded CSV file could not be read.', 'cartflush' ) );
    213113        }
    214 
    215114        $header = fgetcsv( $handle );
    216 
    217115        if ( ! is_array( $header ) ) {
    218             fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
     116            fclose( $handle ); // phpcs:ignore
    219117            $this->redirect_with_message( '', __( 'The CSV file is empty.', 'cartflush' ) );
    220118        }
    221 
    222119        $header = array_map( 'sanitize_key', $header );
    223120        $rules  = $this->rules->get_rules_option();
    224 
    225121        while ( false !== ( $row = fgetcsv( $handle ) ) ) {
    226             $row = array_pad( $row, count( $header ), '' );
    227             $row = array_combine( $header, $row );
    228 
     122            $row = array_combine( $header, array_pad( $row, count( $header ), '' ) );
    229123            if ( ! is_array( $row ) ) {
    230124                continue;
    231125            }
    232 
    233126            $type    = isset( $row['type'] ) ? sanitize_key( $row['type'] ) : '';
    234             $key     = isset( $row['key'] ) ? $row['key'] : '';
     127            $key     = isset( $row['key'] ) ? trim( (string) $row['key'] ) : '';
    235128            $timeout = isset( $row['timeout_minutes'] ) ? absint( $row['timeout_minutes'] ) : 0;
    236 
    237             if ( ! $type || '' === trim( $key ) ) {
     129            if ( ! $type || '' === $key ) {
    238130                continue;
    239131            }
    240 
    241132            switch ( $type ) {
     133                case 'customer_type':
     134                    if ( in_array( sanitize_key( $key ), [ 'guest', 'logged_in' ], true ) && $timeout > 0 ) {
     135                        $rules['customer_type_rules'][ sanitize_key( $key ) ] = $timeout;
     136                    }
     137                    break;
    242138                case 'role':
    243139                    if ( $timeout > 0 ) {
     
    245141                    }
    246142                    break;
    247 
     143                case 'product_rule':
     144                    if ( absint( $key ) > 0 && $timeout > 0 ) {
     145                        $rules['product_rules'][ absint( $key ) ] = $timeout;
     146                    }
     147                    break;
    248148                case 'category':
    249149                    if ( $timeout > 0 ) {
     
    251151                    }
    252152                    break;
    253 
     153                case 'tag':
     154                    if ( $timeout > 0 ) {
     155                        $rules['tag_rules'][ sanitize_title( $key ) ] = $timeout;
     156                    }
     157                    break;
     158                case 'excluded_role':
     159                    $rules['excluded_roles'][] = sanitize_key( $key );
     160                    break;
    254161                case 'excluded_product':
    255162                case 'product':
    256                     $product_id = absint( $key );
    257                     if ( $product_id > 0 ) {
    258                         $rules['excluded_products'][] = $product_id;
     163                    if ( absint( $key ) > 0 ) {
     164                        $rules['excluded_products'][] = absint( $key );
    259165                    }
    260166                    break;
    261 
    262167                case 'excluded_category':
    263168                    $rules['excluded_categories'][] = sanitize_title( $key );
    264169                    break;
     170                case 'excluded_tag':
     171                    $rules['excluded_tags'][] = sanitize_title( $key );
     172                    break;
    265173            }
    266174        }
    267 
    268         fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
    269 
     175        fclose( $handle ); // phpcs:ignore
    270176        update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) );
    271 
    272177        $this->redirect_with_message( __( 'CSV rules imported successfully.', 'cartflush' ) );
    273178    }
    274179
    275     /**
    276      * Export JSON settings.
    277      *
    278      * @return void
    279      */
    280180    public function handle_json_export() {
    281181        $this->assert_admin_permissions();
    282182        check_admin_referer( 'cartflush_export_json' );
    283 
    284         $payload = [
    285             'plugin'                    => 'CartFlush',
    286             'version'                   => CARTFLUSH_VERSION,
    287             'exported_at'               => gmdate( 'c' ),
    288             'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ),
    289             'import_rules'              => $this->rules->get_rules_option(),
    290         ];
    291 
     183        $payload = [ 'plugin' => 'CartFlush', 'version' => CARTFLUSH_VERSION, 'exported_at' => gmdate( 'c' ), 'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ), 'import_rules' => $this->rules->get_rules_option() ];
    292184        nocache_headers();
    293185        header( 'Content-Type: application/json; charset=utf-8' );
    294186        header( 'Content-Disposition: attachment; filename=cartflush-settings-' . gmdate( 'Y-m-d' ) . '.json' );
    295 
    296187        echo wp_json_encode( $payload, JSON_PRETTY_PRINT );
    297188        exit;
    298189    }
    299190
    300     /**
    301      * Render the admin page.
    302      *
    303      * @return void
    304      */
    305191    public function settings_page_html() {
    306         $rules = $this->rules->get_rules_option();
     192        $rules          = $this->rules->get_rules_option();
     193        $roles          = $this->get_role_options();
     194        $customer_types = [ 'guest' => __( 'Guest', 'cartflush' ), 'logged_in' => __( 'Logged-in', 'cartflush' ) ];
     195        $categories     = $this->get_taxonomy_options( 'product_cat' );
     196        $tags           = $this->get_taxonomy_options( 'product_tag' );
    307197        ?>
    308198        <div class="wrap cartflush-admin">
    309             <div class="cartflush-hero">
    310                 <div>
    311                     <h1><?php esc_html_e( 'CartFlush Settings', 'cartflush' ); ?></h1>
    312                     <p><?php esc_html_e( 'Protect your WooCommerce experience with rule-based cart cleanup, import/export tools, and product-aware exclusions.', 'cartflush' ); ?></p>
     199            <div class="cartflush-shell">
     200                <section class="cartflush-hero">
     201                    <div class="cartflush-hero__content">
     202                        <span class="cartflush-eyebrow"><?php esc_html_e( 'WooCommerce Settings', 'cartflush' ); ?></span>
     203                        <h1><?php esc_html_e( 'CartFlush', 'cartflush' ); ?></h1>
     204                        <p><?php esc_html_e( 'Configure automatic cart clearing with layered timeout rules, product conditions, and exclusions from one settings page.', 'cartflush' ); ?></p>
     205                        <div class="cartflush-hero__stats">
     206                            <?php foreach ( $this->get_stats( $rules ) as $stat ) : ?>
     207                                <div class="cartflush-stat">
     208                                    <span class="cartflush-stat__label"><?php echo esc_html( $stat['label'] ); ?></span>
     209                                    <strong><?php echo esc_html( $stat['value'] ); ?></strong>
     210                                    <span class="cartflush-stat__meta"><?php echo esc_html( $stat['meta'] ); ?></span>
     211                                </div>
     212                            <?php endforeach; ?>
     213                        </div>
     214                    </div>
     215                    <div class="cartflush-hero__rail">
     216                        <div class="cartflush-rail-card">
     217                            <span class="cartflush-chip cartflush-chip--blue"><?php esc_html_e( 'How It Works', 'cartflush' ); ?></span>
     218                            <h3><?php esc_html_e( 'Build rules directly from this screen', 'cartflush' ); ?></h3>
     219                            <p><?php esc_html_e( 'Use a default timeout, layer rule-based overrides, then add exclusions for carts that should never be cleared automatically.', 'cartflush' ); ?></p>
     220                            <ul class="cartflush-bullet-list">
     221                                <li><?php esc_html_e( 'Customer type and role timeouts', 'cartflush' ); ?></li>
     222                                <li><?php esc_html_e( 'Product, category, and tag rules', 'cartflush' ); ?></li>
     223                                <li><?php esc_html_e( 'Role, product, category, and tag exclusions', 'cartflush' ); ?></li>
     224                            </ul>
     225                        </div>
     226                    </div>
     227                </section>
     228
     229                <div class="cartflush-layout">
     230                    <main class="cartflush-main">
     231                        <form method="post" action="options.php" class="cartflush-settings-form">
     232                            <section class="cartflush-panel">
     233                                <div class="cartflush-panel__intro">
     234                                    <div>
     235                                        <span class="cartflush-chip"><?php esc_html_e( 'General', 'cartflush' ); ?></span>
     236                                        <h2><?php esc_html_e( 'General Settings', 'cartflush' ); ?></h2>
     237                                        <p><?php esc_html_e( 'Choose the fallback timeout used when no more specific rule applies.', 'cartflush' ); ?></p>
     238                                    </div>
     239                                </div>
     240                                <?php settings_fields( 'cartflush_settings_group' ); do_settings_sections( 'cartflush-settings' ); ?>
     241                            </section>
     242
     243                            <section class="cartflush-panel">
     244                                <div class="cartflush-panel__intro">
     245                                    <div>
     246                                        <span class="cartflush-chip"><?php esc_html_e( 'Timeout Rules', 'cartflush' ); ?></span>
     247                                        <h2><?php esc_html_e( 'Rule-Based Timeouts', 'cartflush' ); ?></h2>
     248                                        <p><?php esc_html_e( 'Apply shorter or longer cart expiration windows based on the customer or the products in the cart.', 'cartflush' ); ?></p>
     249                                    </div>
     250                                </div>
     251                                <div class="cartflush-rule-grid">
     252                                    <?php
     253                                    $this->render_rule_card( __( 'Customer Type Rules', 'cartflush' ), __( 'Set separate timeouts for guest and logged-in customers.', 'cartflush' ), 'customer-type', __( 'Add Rule', 'cartflush' ), [ __( 'Customer Type', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['customer_type_rules'] ), __( 'Use this when guest carts and account carts should expire differently.', 'cartflush' ), function() use ( $rules, $customer_types ) { $this->render_customer_type_rows( $rules['customer_type_rules'], $customer_types ); } );
     254                                    $this->render_rule_card( __( 'Role Rules', 'cartflush' ), __( 'Override the default timeout for specific user roles.', 'cartflush' ), 'role-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Role', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['role_rules'] ), __( 'Perfect for wholesale, shop manager, or VIP-specific behavior.', 'cartflush' ), function() use ( $rules, $roles ) { $this->render_map_timeout_rows( 'role_rules', 'role', $rules['role_rules'], $roles, __( 'Select a role', 'cartflush' ) ); } );
     255                                    $this->render_rule_card( __( 'Product Rules', 'cartflush' ), __( 'Set a timeout for a single WooCommerce product ID.', 'cartflush' ), 'product-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Product ID', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['product_rules'] ), __( 'Use product-level rules when one item needs a special cart lifetime.', 'cartflush' ), function() use ( $rules ) { $this->render_product_timeout_rows( 'product_rules', $rules['product_rules'] ); } );
     256                                    $this->render_rule_card( __( 'Category Rules', 'cartflush' ), __( 'Apply timeouts using WooCommerce product categories.', 'cartflush' ), 'category-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Category', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['category_rules'] ), __( 'Great for grouping timeouts across collections instead of product by product.', 'cartflush' ), function() use ( $rules, $categories ) { $this->render_map_timeout_rows( 'category_rules', 'slug', $rules['category_rules'], $categories, __( 'Select a category', 'cartflush' ) ); } );
     257                                    $this->render_rule_card( __( 'Tag Rules', 'cartflush' ), __( 'Apply timeouts using WooCommerce product tags.', 'cartflush' ), 'tag-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Tag', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['tag_rules'] ), __( 'Helpful for campaign, seasonal, or merchandising tag-based rules.', 'cartflush' ), function() use ( $rules, $tags ) { $this->render_map_timeout_rows( 'tag_rules', 'slug', $rules['tag_rules'], $tags, __( 'Select a tag', 'cartflush' ) ); } );
     258                                    ?>
     259                                </div>
     260                            </section>
     261
     262                            <section class="cartflush-panel">
     263                                <div class="cartflush-panel__intro">
     264                                    <div>
     265                                        <span class="cartflush-chip"><?php esc_html_e( 'Exclusions', 'cartflush' ); ?></span>
     266                                        <h2><?php esc_html_e( 'Exclusion Rules', 'cartflush' ); ?></h2>
     267                                        <p><?php esc_html_e( 'Skip the auto-clear behavior completely when a cart matches one of the following conditions.', 'cartflush' ); ?></p>
     268                                    </div>
     269                                </div>
     270                                <div class="cartflush-rule-grid">
     271                                    <?php
     272                                    $this->render_rule_card( __( 'Excluded Roles', 'cartflush' ), __( 'Exclude selected user roles from cart clearing.', 'cartflush' ), 'excluded-role', __( 'Add Exclusion', 'cartflush' ), [ __( 'Role', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_roles'] ), __( 'These roles will always bypass CartFlush regardless of timeout rules.', 'cartflush' ), function() use ( $rules, $roles ) { $this->render_simple_select_rows( 'excluded_roles', 'role', $rules['excluded_roles'], $roles, __( 'Select a role', 'cartflush' ) ); } );
     273                                    $this->render_rule_card( __( 'Excluded Products', 'cartflush' ), __( 'Exclude carts that contain any of these products.', 'cartflush' ), 'excluded-product', __( 'Add Exclusion', 'cartflush' ), [ __( 'Product ID', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_products'] ), __( 'Useful for subscriptions, booking items, or products needing longer retention.', 'cartflush' ), function() use ( $rules ) { $this->render_simple_number_rows( 'excluded_products', 'product_id', $rules['excluded_products'] ); } );
     274                                    $this->render_rule_card( __( 'Excluded Categories', 'cartflush' ), __( 'Exclude carts that contain products in these categories.', 'cartflush' ), 'excluded-category', __( 'Add Exclusion', 'cartflush' ), [ __( 'Category', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_categories'] ), __( 'Protect full product collections from the auto-clear workflow.', 'cartflush' ), function() use ( $rules, $categories ) { $this->render_simple_select_rows( 'excluded_categories', 'slug', $rules['excluded_categories'], $categories, __( 'Select a category', 'cartflush' ) ); } );
     275                                    $this->render_rule_card( __( 'Excluded Tags', 'cartflush' ), __( 'Exclude carts that contain products with these tags.', 'cartflush' ), 'excluded-tag', __( 'Add Exclusion', 'cartflush' ), [ __( 'Tag', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_tags'] ), __( 'Best for campaigns or fulfillment groups that should never be auto-cleared.', 'cartflush' ), function() use ( $rules, $tags ) { $this->render_simple_select_rows( 'excluded_tags', 'slug', $rules['excluded_tags'], $tags, __( 'Select a tag', 'cartflush' ) ); } );
     276                                    ?>
     277                                </div>
     278                            </section>
     279
     280                            <div class="cartflush-savebar">
     281                                <div>
     282                                    <strong><?php esc_html_e( 'Save changes', 'cartflush' ); ?></strong>
     283                                    <p><?php esc_html_e( 'Your new rules will be used immediately for future inactivity checks.', 'cartflush' ); ?></p>
     284                                </div>
     285                                <?php submit_button( __( 'Save Settings', 'cartflush' ), 'primary cartflush-savebar__button', 'submit', false ); ?>
     286                            </div>
     287                        </form>
     288                    </main>
     289
     290                    <aside class="cartflush-sidebar">
     291                        <div class="cartflush-sidepanel">
     292                            <div class="cartflush-sidepanel__intro">
     293                                <span class="cartflush-chip"><?php esc_html_e( 'Tools', 'cartflush' ); ?></span>
     294                                <h2><?php esc_html_e( 'Import & Export', 'cartflush' ); ?></h2>
     295                                <p><?php esc_html_e( 'Bring settings in from another store or create a backup before making large rule changes.', 'cartflush' ); ?></p>
     296                            </div>
     297
     298                            <div class="cartflush-sidecard">
     299                                <div class="cartflush-sidecard__header">
     300                                    <span class="cartflush-chip"><?php esc_html_e( 'CSV Import', 'cartflush' ); ?></span>
     301                                    <strong><?php esc_html_e( 'Bulk Rules', 'cartflush' ); ?></strong>
     302                                </div>
     303                                <p><?php esc_html_e( 'Upload a CSV using the headers type, key, timeout_minutes. Exclusion rows can keep timeout_minutes empty.', 'cartflush' ); ?></p>
     304                                <div class="cartflush-code-list">
     305                                    <code>customer_type</code>
     306                                    <code>role</code>
     307                                    <code>product_rule</code>
     308                                    <code>category</code>
     309                                    <code>tag</code>
     310                                </div>
     311                                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
     312                                    <?php wp_nonce_field( 'cartflush_import_csv' ); ?>
     313                                    <input type="hidden" name="action" value="cartflush_import_csv">
     314                                    <label class="cartflush-upload-field">
     315                                        <span><?php esc_html_e( 'Choose CSV file', 'cartflush' ); ?></span>
     316                                        <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required>
     317                                    </label>
     318                                    <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?>
     319                                </form>
     320                            </div>
     321
     322                            <div class="cartflush-sidecard">
     323                                <div class="cartflush-sidecard__header">
     324                                    <span class="cartflush-chip"><?php esc_html_e( 'JSON Import', 'cartflush' ); ?></span>
     325                                    <strong><?php esc_html_e( 'Move Settings', 'cartflush' ); ?></strong>
     326                                </div>
     327                                <p><?php esc_html_e( 'Import a full CartFlush configuration from another WooCommerce store.', 'cartflush' ); ?></p>
     328                                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
     329                                    <?php wp_nonce_field( 'cartflush_import_json' ); ?>
     330                                    <input type="hidden" name="action" value="cartflush_import_json">
     331                                    <label class="cartflush-upload-field">
     332                                        <span><?php esc_html_e( 'Choose JSON file', 'cartflush' ); ?></span>
     333                                        <input type="file" name="cartflush_json_file" accept=".json,application/json" required>
     334                                    </label>
     335                                    <?php submit_button( __( 'Import JSON', 'cartflush' ), 'secondary', 'submit', false ); ?>
     336                                </form>
     337                            </div>
     338
     339                            <div class="cartflush-sidecard">
     340                                <div class="cartflush-sidecard__header">
     341                                    <span class="cartflush-chip"><?php esc_html_e( 'JSON Export', 'cartflush' ); ?></span>
     342                                    <strong><?php esc_html_e( 'Backup', 'cartflush' ); ?></strong>
     343                                </div>
     344                                <p><?php esc_html_e( 'Download the current timeout and every saved rule group as a portable JSON backup.', 'cartflush' ); ?></p>
     345                                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
     346                                    <?php wp_nonce_field( 'cartflush_export_json' ); ?>
     347                                    <input type="hidden" name="action" value="cartflush_export_json">
     348                                    <?php submit_button( __( 'Export JSON', 'cartflush' ), 'secondary', 'submit', false ); ?>
     349                                </form>
     350                            </div>
     351                        </div>
     352                    </aside>
    313353                </div>
    314                 <div class="cartflush-hero__meta">
    315                     <span><?php esc_html_e( 'Version', 'cartflush' ); ?> <?php echo esc_html( CARTFLUSH_VERSION ); ?></span>
    316                     <span><?php esc_html_e( 'Settings', 'cartflush' ); ?></span>
    317                 </div>
     354
     355                <section class="cartflush-summary-panel">
     356                    <div class="cartflush-panel__intro">
     357                        <div>
     358                            <span class="cartflush-chip"><?php esc_html_e( 'Overview', 'cartflush' ); ?></span>
     359                            <h2><?php esc_html_e( 'Saved Configuration', 'cartflush' ); ?></h2>
     360                        </div>
     361                    </div>
     362                    <div class="cartflush-summary">
     363                        <?php foreach ( $this->get_summary_items( $rules ) as $summary ) : ?>
     364                            <div><h3><?php echo esc_html( $summary['label'] ); ?></h3><p><?php echo esc_html( $summary['value'] ); ?></p></div>
     365                        <?php endforeach; ?>
     366                    </div>
     367                </section>
    318368            </div>
    319369
    320             <div class="cartflush-grid">
    321                 <div class="cartflush-card cartflush-card--primary">
    322                     <h2><?php esc_html_e( 'Default Timeout', 'cartflush' ); ?></h2>
    323                     <form method="post" action="options.php">
    324                         <?php
    325                         settings_fields( 'cartflush_settings_group' );
    326                         do_settings_sections( 'cartflush-settings' );
    327                         submit_button( __( 'Save Settings', 'cartflush' ) );
    328                         ?>
    329                     </form>
    330                 </div>
    331 
    332                 <div class="cartflush-card">
    333                     <h2><?php esc_html_e( 'Import Full Settings', 'cartflush' ); ?></h2>
    334                     <p><?php esc_html_e( 'Upload a JSON export to move CartFlush settings between sites.', 'cartflush' ); ?></p>
    335                     <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
    336                         <?php wp_nonce_field( 'cartflush_import_json' ); ?>
    337                         <input type="hidden" name="action" value="cartflush_import_json">
    338                         <input type="file" name="cartflush_json_file" accept=".json,application/json" required>
    339                         <?php submit_button( __( 'Import JSON', 'cartflush' ), 'secondary', 'submit', false ); ?>
    340                     </form>
    341                 </div>
    342 
    343                 <div class="cartflush-card">
    344                     <h2><?php esc_html_e( 'Import Rules from CSV', 'cartflush' ); ?></h2>
    345                     <p><?php esc_html_e( 'Use type, key, and timeout_minutes columns to import role rules, category rules, or exclusions.', 'cartflush' ); ?></p>
    346                     <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
    347                         <?php wp_nonce_field( 'cartflush_import_csv' ); ?>
    348                         <input type="hidden" name="action" value="cartflush_import_csv">
    349                         <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required>
    350                         <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?>
    351                     </form>
    352                     <p class="description"><code>role,customer,30</code> <code>category,subscription-box,10</code> <code>excluded_product,123,</code></p>
    353                 </div>
    354 
    355                 <div class="cartflush-card">
    356                     <h2><?php esc_html_e( 'Export Settings', 'cartflush' ); ?></h2>
    357                     <p><?php esc_html_e( 'Download the current timeout, imported rules, and exclusions as a JSON snapshot.', 'cartflush' ); ?></p>
    358                     <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    359                         <?php wp_nonce_field( 'cartflush_export_json' ); ?>
    360                         <input type="hidden" name="action" value="cartflush_export_json">
    361                         <?php submit_button( __( 'Export JSON', 'cartflush' ), 'secondary', 'submit', false ); ?>
    362                     </form>
    363                 </div>
    364             </div>
    365 
    366             <div class="cartflush-card cartflush-card--wide">
    367                 <h2><?php esc_html_e( 'Imported Configuration Summary', 'cartflush' ); ?></h2>
    368                 <div class="cartflush-summary">
    369                     <div>
    370                         <h3><?php esc_html_e( 'Role Rules', 'cartflush' ); ?></h3>
    371                         <p><?php echo esc_html( $this->format_assoc_list( $rules['role_rules'] ) ); ?></p>
    372                     </div>
    373                     <div>
    374                         <h3><?php esc_html_e( 'Category Rules', 'cartflush' ); ?></h3>
    375                         <p><?php echo esc_html( $this->format_assoc_list( $rules['category_rules'] ) ); ?></p>
    376                     </div>
    377                     <div>
    378                         <h3><?php esc_html_e( 'Excluded Product IDs', 'cartflush' ); ?></h3>
    379                         <p><?php echo esc_html( $this->format_simple_list( $rules['excluded_products'] ) ); ?></p>
    380                     </div>
    381                     <div>
    382                         <h3><?php esc_html_e( 'Excluded Category Slugs', 'cartflush' ); ?></h3>
    383                         <p><?php echo esc_html( $this->format_simple_list( $rules['excluded_categories'] ) ); ?></p>
    384                     </div>
    385                 </div>
    386             </div>
     370            <?php $this->render_templates( $roles, $customer_types, $categories, $tags ); ?>
    387371        </div>
    388372        <?php
    389373    }
    390374
    391     /**
    392      * Format key-value lists for display.
    393      *
    394      * @param array<string, int> $items Display items.
    395      * @return string
    396      */
     375    private function render_rule_card( $title, $description, $type, $button_label, $headers, $count, $hint, $renderer ) {
     376        ?>
     377        <div class="cartflush-rule-card">
     378            <div class="cartflush-rule-card__header">
     379                <div class="cartflush-rule-card__title">
     380                    <div class="cartflush-rule-card__meta">
     381                        <span class="cartflush-rule-card__count"><?php echo esc_html( absint( $count ) ); ?></span>
     382                        <span class="cartflush-rule-card__label"><?php esc_html_e( 'saved', 'cartflush' ); ?></span>
     383                    </div>
     384                    <h3><?php echo esc_html( $title ); ?></h3>
     385                    <p><?php echo esc_html( $description ); ?></p>
     386                </div>
     387                <button type="button" class="button cartflush-action" data-cartflush-add-row="<?php echo esc_attr( $type ); ?>"><?php echo esc_html( $button_label ); ?></button>
     388            </div>
     389            <div class="cartflush-table-wrap">
     390                <table class="widefat striped cartflush-rule-table">
     391                    <thead><tr><?php foreach ( $headers as $index => $header ) : ?><th class="<?php echo esc_attr( $index === count( $headers ) - 1 ? 'cartflush-rule-table__action' : '' ); ?>"><?php echo esc_html( $header ); ?></th><?php endforeach; ?></tr></thead>
     392                    <tbody data-cartflush-rows="<?php echo esc_attr( $type ); ?>"><?php call_user_func( $renderer ); ?></tbody>
     393                </table>
     394            </div>
     395            <p class="cartflush-rule-card__hint"><?php echo esc_html( $hint ); ?></p>
     396        </div>
     397        <?php
     398    }
     399
     400    private function render_templates( $roles, $customer_types, $categories, $tags ) {
     401        ?>
     402        <script type="text/html" id="tmpl-cartflush-customer-type"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[customer_type_rules][{{index}}][type]', '', $customer_types, __( 'Select customer type', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[customer_type_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     403        <script type="text/html" id="tmpl-cartflush-role-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[role_rules][{{index}}][role]', '', $roles, __( 'Select a role', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[role_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     404        <script type="text/html" id="tmpl-cartflush-product-rule"><tr><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][product_id]" value=""></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     405        <script type="text/html" id="tmpl-cartflush-category-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[category_rules][{{index}}][slug]', '', $categories, __( 'Select a category', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[category_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     406        <script type="text/html" id="tmpl-cartflush-tag-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[tag_rules][{{index}}][slug]', '', $tags, __( 'Select a tag', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[tag_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     407        <script type="text/html" id="tmpl-cartflush-excluded-role"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_roles][{{index}}][role]', '', $roles, __( 'Select a role', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     408        <script type="text/html" id="tmpl-cartflush-excluded-product"><tr><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[excluded_products][{{index}}][product_id]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     409        <script type="text/html" id="tmpl-cartflush-excluded-category"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_categories][{{index}}][slug]', '', $categories, __( 'Select a category', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     410        <script type="text/html" id="tmpl-cartflush-excluded-tag"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_tags][{{index}}][slug]', '', $tags, __( 'Select a tag', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script>
     411        <?php
     412    }
     413
     414    private function render_customer_type_rows( $items, $options ) {
     415        if ( empty( $items ) ) { echo $this->get_select_timeout_row( 'customer_type_rules', 0, 'type', '', '', $options, __( 'Select customer type', 'cartflush' ) ); return; } // phpcs:ignore
     416        $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_select_timeout_row( 'customer_type_rules', $index, 'type', $key, $timeout, $options, __( 'Select customer type', 'cartflush' ) ); ++$index; } // phpcs:ignore
     417    }
     418
     419    private function render_map_timeout_rows( $group, $field, $items, $options, $placeholder ) {
     420        if ( empty( $items ) ) { echo $this->get_select_timeout_row( $group, 0, $field, '', '', $options, $placeholder ); return; } // phpcs:ignore
     421        $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_select_timeout_row( $group, $index, $field, $key, $timeout, $options, $placeholder ); ++$index; } // phpcs:ignore
     422    }
     423
     424    private function render_product_timeout_rows( $group, $items ) {
     425        if ( empty( $items ) ) { echo $this->get_number_timeout_row( $group, 0, '', '' ); return; } // phpcs:ignore
     426        $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_number_timeout_row( $group, $index, $key, $timeout ); ++$index; } // phpcs:ignore
     427    }
     428
     429    private function render_simple_select_rows( $group, $field, $items, $options, $placeholder ) {
     430        if ( empty( $items ) ) { echo $this->get_simple_select_row( $group, 0, $field, '', $options, $placeholder ); return; } // phpcs:ignore
     431        foreach ( array_values( $items ) as $index => $item ) { echo $this->get_simple_select_row( $group, $index, $field, $item, $options, $placeholder ); } // phpcs:ignore
     432    }
     433
     434    private function render_simple_number_rows( $group, $field, $items ) {
     435        if ( empty( $items ) ) { echo $this->get_simple_number_row( $group, 0, $field, '' ); return; } // phpcs:ignore
     436        foreach ( array_values( $items ) as $index => $item ) { echo $this->get_simple_number_row( $group, $index, $field, $item ); } // phpcs:ignore
     437    }
     438
     439    private function get_select_timeout_row( $group, $index, $field, $value, $timeout, $options, $placeholder ) {
     440        ob_start(); ?><tr><td><?php $this->render_select_field( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']', (string) $value, $options, $placeholder ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean();
     441    }
     442
     443    private function get_number_timeout_row( $group, $index, $value, $timeout ) {
     444        ob_start(); ?><tr><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][product_id]' ); ?>" value="<?php echo esc_attr( $value ); ?>"></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean();
     445    }
     446
     447    private function get_simple_select_row( $group, $index, $field, $value, $options, $placeholder ) {
     448        ob_start(); ?><tr><td><?php $this->render_select_field( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']', (string) $value, $options, $placeholder ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean();
     449    }
     450
     451    private function get_simple_number_row( $group, $index, $field, $value ) {
     452        ob_start(); ?><tr><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']' ); ?>" value="<?php echo esc_attr( $value ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean();
     453    }
     454
     455    private function render_select_field( $name, $selected, $options, $placeholder ) {
     456        $options = is_array( $options ) ? $options : [];
     457        if ( $selected && ! isset( $options[ $selected ] ) ) { $options = [ $selected => $selected ] + $options; }
     458        ?><select class="regular-text" name="<?php echo esc_attr( $name ); ?>"><option value=""><?php echo esc_html( $placeholder ); ?></option><?php foreach ( $options as $value => $label ) : ?><option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected, (string) $value ); ?>><?php echo esc_html( $label ); ?></option><?php endforeach; ?></select><?php
     459    }
     460
     461    private function render_remove_button() {
     462        echo '<button type="button" class="button-link-delete cartflush-remove-row">' . esc_html__( 'Remove', 'cartflush' ) . '</button>';
     463    }
     464
     465    private function prepare_rules_for_storage( $value ) {
     466        return [
     467            'customer_type_rules' => $this->prepare_timeout_rows( isset( $value['customer_type_rules'] ) ? $value['customer_type_rules'] : [], 'type', 'sanitize_key' ),
     468            'role_rules'          => $this->prepare_timeout_rows( isset( $value['role_rules'] ) ? $value['role_rules'] : [], 'role', 'sanitize_key' ),
     469            'product_rules'       => $this->prepare_integer_timeout_rows( isset( $value['product_rules'] ) ? $value['product_rules'] : [], 'product_id' ),
     470            'category_rules'      => $this->prepare_timeout_rows( isset( $value['category_rules'] ) ? $value['category_rules'] : [], 'slug', 'sanitize_title' ),
     471            'tag_rules'           => $this->prepare_timeout_rows( isset( $value['tag_rules'] ) ? $value['tag_rules'] : [], 'slug', 'sanitize_title' ),
     472            'excluded_roles'      => $this->prepare_string_list_rows( isset( $value['excluded_roles'] ) ? $value['excluded_roles'] : [], 'role', 'sanitize_key' ),
     473            'excluded_products'   => $this->prepare_integer_list_rows( isset( $value['excluded_products'] ) ? $value['excluded_products'] : [], 'product_id' ),
     474            'excluded_categories' => $this->prepare_string_list_rows( isset( $value['excluded_categories'] ) ? $value['excluded_categories'] : [], 'slug', 'sanitize_title' ),
     475            'excluded_tags'       => $this->prepare_string_list_rows( isset( $value['excluded_tags'] ) ? $value['excluded_tags'] : [], 'slug', 'sanitize_title' ),
     476        ];
     477    }
     478
     479    private function prepare_timeout_rows( $rows, $field, $sanitizer ) {
     480        $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $key = isset( $row[ $field ] ) ? call_user_func( $sanitizer, $row[ $field ] ) : ''; $timeout = isset( $row['timeout'] ) ? absint( $row['timeout'] ) : 0; if ( $key && $timeout > 0 ) { $prepared[ $key ] = $timeout; } } return $prepared;
     481    }
     482
     483    private function prepare_integer_timeout_rows( $rows, $field ) {
     484        $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $key = isset( $row[ $field ] ) ? absint( $row[ $field ] ) : 0; $timeout = isset( $row['timeout'] ) ? absint( $row['timeout'] ) : 0; if ( $key > 0 && $timeout > 0 ) { $prepared[ $key ] = $timeout; } } return $prepared;
     485    }
     486
     487    private function prepare_string_list_rows( $rows, $field, $sanitizer ) {
     488        $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { $item = is_array( $row ) && isset( $row[ $field ] ) ? call_user_func( $sanitizer, $row[ $field ] ) : ''; if ( $item ) { $prepared[] = $item; } } return array_values( array_unique( $prepared ) );
     489    }
     490
     491    private function prepare_integer_list_rows( $rows, $field ) {
     492        $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { $item = is_array( $row ) && isset( $row[ $field ] ) ? absint( $row[ $field ] ) : 0; if ( $item > 0 ) { $prepared[] = $item; } } return array_values( array_unique( $prepared ) );
     493    }
     494
     495    private function get_stats( $rules ) {
     496        return [
     497            [ 'label' => __( 'Default Timeout', 'cartflush' ), 'value' => (int) get_option( 'cartflush_expiration_time', 30 ), 'meta' => __( 'minutes', 'cartflush' ) ],
     498            [ 'label' => __( 'Timeout Rules', 'cartflush' ), 'value' => count( $rules['customer_type_rules'] ) + count( $rules['role_rules'] ) + count( $rules['product_rules'] ) + count( $rules['category_rules'] ) + count( $rules['tag_rules'] ), 'meta' => __( 'active', 'cartflush' ) ],
     499            [ 'label' => __( 'Exclusions', 'cartflush' ), 'value' => count( $rules['excluded_roles'] ) + count( $rules['excluded_products'] ) + count( $rules['excluded_categories'] ) + count( $rules['excluded_tags'] ), 'meta' => __( 'guards', 'cartflush' ) ],
     500            [ 'label' => __( 'Import Modes', 'cartflush' ), 'value' => 'CSV + JSON', 'meta' => __( 'ready', 'cartflush' ) ],
     501        ];
     502    }
     503
     504    private function get_summary_items( $rules ) {
     505        return [
     506            [ 'label' => __( 'Customer Type Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['customer_type_rules'] ) ],
     507            [ 'label' => __( 'Role Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['role_rules'] ) ],
     508            [ 'label' => __( 'Product Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['product_rules'] ) ],
     509            [ 'label' => __( 'Category Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['category_rules'] ) ],
     510            [ 'label' => __( 'Tag Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['tag_rules'] ) ],
     511            [ 'label' => __( 'Excluded Roles', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_roles'] ) ],
     512            [ 'label' => __( 'Excluded Products', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_products'] ) ],
     513            [ 'label' => __( 'Excluded Categories', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_categories'] ) ],
     514            [ 'label' => __( 'Excluded Tags', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_tags'] ) ],
     515        ];
     516    }
     517
     518    private function get_role_options() {
     519        if ( ! function_exists( 'wp_roles' ) ) { return []; }
     520        $options = []; foreach ( wp_roles()->roles as $key => $data ) { $options[ sanitize_key( $key ) ] = isset( $data['name'] ) ? $data['name'] : $key; } return $options;
     521    }
     522
     523    private function get_taxonomy_options( $taxonomy ) {
     524        $options = []; $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false ] ); if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { return $options; } foreach ( $terms as $term ) { if ( isset( $term->slug, $term->name ) ) { $options[ $term->slug ] = $term->name . ' (' . $term->slug . ')'; } } return $options;
     525    }
     526
    397527    private function format_assoc_list( $items ) {
    398         if ( empty( $items ) ) {
    399             return __( 'None imported yet.', 'cartflush' );
    400         }
    401 
    402         $formatted = [];
    403 
    404         foreach ( $items as $key => $value ) {
    405             $formatted[] = sprintf(
    406                 /* translators: 1: rule key, 2: timeout in minutes. */
    407                 __( '%1$s: %2$d min', 'cartflush' ),
    408                 $key,
    409                 absint( $value )
    410             );
    411         }
    412 
    413         return implode( ', ', $formatted );
    414     }
    415 
    416     /**
    417      * Format flat lists for display.
    418      *
    419      * @param array<int|string> $items Display items.
    420      * @return string
    421      */
     528        if ( empty( $items ) ) { return __( 'None saved yet.', 'cartflush' ); }
     529        $formatted = []; foreach ( $items as $key => $value ) { $formatted[] = sprintf( __( '%1$s: %2$d min', 'cartflush' ), (string) $key, absint( $value ) ); } return implode( ', ', $formatted );
     530    }
     531
    422532    private function format_simple_list( $items ) {
    423         if ( empty( $items ) ) {
    424             return __( 'None imported yet.', 'cartflush' );
    425         }
    426 
    427         return implode( ', ', array_map( 'strval', $items ) );
    428     }
    429 
    430     /**
    431      * Ensure the current user can manage the plugin.
    432      *
    433      * @return void
    434      */
     533        return empty( $items ) ? __( 'None saved yet.', 'cartflush' ) : implode( ', ', array_map( 'strval', $items ) );
     534    }
     535
    435536    private function assert_admin_permissions() {
    436         if ( ! current_user_can( 'manage_options' ) ) {
    437             wp_die( esc_html__( 'You do not have permission to access this page.', 'cartflush' ) );
    438         }
    439     }
    440 
    441     /**
    442      * Redirect back to the settings page with a status message.
    443      *
    444      * @param string $notice Success message.
    445      * @param string $error  Error message.
    446      * @return void
    447      */
     537        if ( ! current_user_can( $this->get_required_capability() ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'cartflush' ) ); }
     538    }
     539
     540    private function get_required_capability() {
     541        return 'manage_woocommerce';
     542    }
     543
    448544    private function redirect_with_message( $notice = '', $error = '' ) {
    449         $url = add_query_arg(
    450             array_filter(
    451                 [
    452                     'page'             => 'cartflush-settings',
    453                     'cartflush_notice' => $notice,
    454                     'cartflush_error'  => $error,
    455                 ]
    456             ),
    457             admin_url( 'options-general.php' )
    458         );
    459 
     545        $url = add_query_arg( array_filter( [ 'page' => 'cartflush-settings', 'cartflush_notice' => $notice, 'cartflush_error' => $error ] ), admin_url( 'admin.php' ) );
    460546        wp_safe_redirect( $url );
    461547        exit;
  • cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-rules.php

    r3484352 r3488606  
    3434        $rules    = wp_parse_args( $value, $defaults );
    3535
    36         $normalized_role_rules = [];
    37         if ( is_array( $rules['role_rules'] ) ) {
    38             foreach ( $rules['role_rules'] as $role => $timeout ) {
    39                 $role    = sanitize_key( $role );
    40                 $timeout = absint( $timeout );
    41 
    42                 if ( $role && $timeout > 0 ) {
    43                     $normalized_role_rules[ $role ] = $timeout;
    44                 }
    45             }
    46         }
    47 
    48         $normalized_category_rules = [];
    49         if ( is_array( $rules['category_rules'] ) ) {
    50             foreach ( $rules['category_rules'] as $slug => $timeout ) {
    51                 $slug    = sanitize_title( $slug );
    52                 $timeout = absint( $timeout );
    53 
    54                 if ( $slug && $timeout > 0 ) {
    55                     $normalized_category_rules[ $slug ] = $timeout;
    56                 }
    57             }
    58         }
    59 
    60         $excluded_products = [];
    61         if ( is_array( $rules['excluded_products'] ) ) {
    62             foreach ( $rules['excluded_products'] as $product_id ) {
    63                 $product_id = absint( $product_id );
    64 
    65                 if ( $product_id > 0 ) {
    66                     $excluded_products[] = $product_id;
    67                 }
    68             }
    69         }
    70 
    71         $excluded_categories = [];
    72         if ( is_array( $rules['excluded_categories'] ) ) {
    73             foreach ( $rules['excluded_categories'] as $slug ) {
    74                 $slug = sanitize_title( $slug );
    75 
    76                 if ( $slug ) {
    77                     $excluded_categories[] = $slug;
    78                 }
    79             }
    80         }
    81 
    8236        return [
    83             'role_rules'          => $normalized_role_rules,
    84             'category_rules'      => $normalized_category_rules,
    85             'excluded_products'   => array_values( array_unique( $excluded_products ) ),
    86             'excluded_categories' => array_values( array_unique( $excluded_categories ) ),
     37            'customer_type_rules' => $this->normalize_customer_type_rules( $rules['customer_type_rules'] ),
     38            'role_rules'          => $this->normalize_timeout_map( $rules['role_rules'], 'sanitize_key' ),
     39            'category_rules'      => $this->normalize_timeout_map( $rules['category_rules'], 'sanitize_title' ),
     40            'tag_rules'           => $this->normalize_timeout_map( $rules['tag_rules'], 'sanitize_title' ),
     41            'product_rules'       => $this->normalize_product_timeout_rules( $rules['product_rules'] ),
     42            'excluded_roles'      => $this->normalize_string_list( $rules['excluded_roles'], 'sanitize_key' ),
     43            'excluded_products'   => $this->normalize_integer_list( $rules['excluded_products'] ),
     44            'excluded_categories' => $this->normalize_string_list( $rules['excluded_categories'], 'sanitize_title' ),
     45            'excluded_tags'       => $this->normalize_string_list( $rules['excluded_tags'], 'sanitize_title' ),
    8746        ];
    8847    }
     
    10261        $user       = wp_get_current_user();
    10362        $cart_items = WC()->cart->get_cart();
     63        $is_guest   = ! ( $user instanceof WP_User ) || 0 === (int) $user->ID;
     64
     65        if ( $is_guest && isset( $rules['customer_type_rules']['guest'] ) ) {
     66            $timeouts[] = (int) $rules['customer_type_rules']['guest'];
     67        }
     68
     69        if ( ! $is_guest && isset( $rules['customer_type_rules']['logged_in'] ) ) {
     70            $timeouts[] = (int) $rules['customer_type_rules']['logged_in'];
     71        }
    10472
    10573        if ( $user instanceof WP_User && ! empty( $user->roles ) ) {
     
    11482
    11583        foreach ( $cart_items as $cart_item ) {
    116             $product_id   = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0;
    117             $category_ids = $product_id ? wc_get_product_term_ids( $product_id, 'product_cat' ) : [];
    118 
    119             foreach ( $category_ids as $category_id ) {
    120                 $term = get_term( $category_id, 'product_cat' );
    121 
    122                 if ( $term && ! is_wp_error( $term ) && isset( $rules['category_rules'][ $term->slug ] ) ) {
    123                     $timeouts[] = (int) $rules['category_rules'][ $term->slug ];
     84            $product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0;
     85
     86            if ( ! $product_id ) {
     87                continue;
     88            }
     89
     90            if ( isset( $rules['product_rules'][ $product_id ] ) ) {
     91                $timeouts[] = (int) $rules['product_rules'][ $product_id ];
     92            }
     93
     94            foreach ( $this->get_product_term_slugs( $product_id, 'product_cat' ) as $slug ) {
     95                if ( isset( $rules['category_rules'][ $slug ] ) ) {
     96                    $timeouts[] = (int) $rules['category_rules'][ $slug ];
     97                }
     98            }
     99
     100            foreach ( $this->get_product_term_slugs( $product_id, 'product_tag' ) as $slug ) {
     101                if ( isset( $rules['tag_rules'][ $slug ] ) ) {
     102                    $timeouts[] = (int) $rules['tag_rules'][ $slug ];
    124103                }
    125104            }
     
    137116
    138117    /**
    139      * Determine whether the current cart contains excluded products or categories.
     118     * Determine whether the current cart contains excluded items.
    140119     *
    141120     * @return bool
     
    147126
    148127        $rules = $this->get_rules_option();
     128        $user  = wp_get_current_user();
     129
     130        if ( $user instanceof WP_User && ! empty( $user->roles ) ) {
     131            foreach ( $user->roles as $role ) {
     132                if ( in_array( sanitize_key( $role ), $rules['excluded_roles'], true ) ) {
     133                    return true;
     134                }
     135            }
     136        }
    149137
    150138        foreach ( WC()->cart->get_cart() as $cart_item ) {
     
    155143            }
    156144
    157             $category_ids = $product_id ? wc_get_product_term_ids( $product_id, 'product_cat' ) : [];
    158 
    159             foreach ( $category_ids as $category_id ) {
    160                 $term = get_term( $category_id, 'product_cat' );
    161 
    162                 if ( $term && ! is_wp_error( $term ) && in_array( $term->slug, $rules['excluded_categories'], true ) ) {
     145            foreach ( $this->get_product_term_slugs( $product_id, 'product_cat' ) as $slug ) {
     146                if ( in_array( $slug, $rules['excluded_categories'], true ) ) {
     147                    return true;
     148                }
     149            }
     150
     151            foreach ( $this->get_product_term_slugs( $product_id, 'product_tag' ) as $slug ) {
     152                if ( in_array( $slug, $rules['excluded_tags'], true ) ) {
    163153                    return true;
    164154                }
     
    176166    public function get_default_rules() {
    177167        return [
     168            'customer_type_rules' => [],
    178169            'role_rules'          => [],
    179170            'category_rules'      => [],
     171            'tag_rules'           => [],
     172            'product_rules'       => [],
     173            'excluded_roles'      => [],
    180174            'excluded_products'   => [],
    181175            'excluded_categories' => [],
     176            'excluded_tags'       => [],
    182177        ];
    183178    }
     179
     180    /**
     181     * Normalize customer type rules.
     182     *
     183     * @param mixed $rules Customer type rules.
     184     * @return array<string, int>
     185     */
     186    private function normalize_customer_type_rules( $rules ) {
     187        $allowed    = [ 'guest', 'logged_in' ];
     188        $normalized = [];
     189
     190        if ( ! is_array( $rules ) ) {
     191            return $normalized;
     192        }
     193
     194        foreach ( $rules as $type => $timeout ) {
     195            $type    = sanitize_key( $type );
     196            $timeout = absint( $timeout );
     197
     198            if ( in_array( $type, $allowed, true ) && $timeout > 0 ) {
     199                $normalized[ $type ] = $timeout;
     200            }
     201        }
     202
     203        return $normalized;
     204    }
     205
     206    /**
     207     * Normalize a generic timeout map.
     208     *
     209     * @param mixed    $rules Timeout rules.
     210     * @param callable $sanitizer Key sanitizer.
     211     * @return array<string, int>
     212     */
     213    private function normalize_timeout_map( $rules, $sanitizer ) {
     214        $normalized = [];
     215
     216        if ( ! is_array( $rules ) ) {
     217            return $normalized;
     218        }
     219
     220        foreach ( $rules as $key => $timeout ) {
     221            $key     = call_user_func( $sanitizer, $key );
     222            $timeout = absint( $timeout );
     223
     224            if ( $key && $timeout > 0 ) {
     225                $normalized[ $key ] = $timeout;
     226            }
     227        }
     228
     229        return $normalized;
     230    }
     231
     232    /**
     233     * Normalize product-specific timeout rules.
     234     *
     235     * @param mixed $rules Product rules.
     236     * @return array<int, int>
     237     */
     238    private function normalize_product_timeout_rules( $rules ) {
     239        $normalized = [];
     240
     241        if ( ! is_array( $rules ) ) {
     242            return $normalized;
     243        }
     244
     245        foreach ( $rules as $product_id => $timeout ) {
     246            $product_id = absint( $product_id );
     247            $timeout    = absint( $timeout );
     248
     249            if ( $product_id > 0 && $timeout > 0 ) {
     250                $normalized[ $product_id ] = $timeout;
     251            }
     252        }
     253
     254        return $normalized;
     255    }
     256
     257    /**
     258     * Normalize a list of integer IDs.
     259     *
     260     * @param mixed $items List value.
     261     * @return array<int>
     262     */
     263    private function normalize_integer_list( $items ) {
     264        $normalized = [];
     265
     266        if ( ! is_array( $items ) ) {
     267            return $normalized;
     268        }
     269
     270        foreach ( $items as $item ) {
     271            $item = absint( $item );
     272
     273            if ( $item > 0 ) {
     274                $normalized[] = $item;
     275            }
     276        }
     277
     278        return array_values( array_unique( $normalized ) );
     279    }
     280
     281    /**
     282     * Normalize a list of strings.
     283     *
     284     * @param mixed    $items List value.
     285     * @param callable $sanitizer Item sanitizer.
     286     * @return array<int, string>
     287     */
     288    private function normalize_string_list( $items, $sanitizer ) {
     289        $normalized = [];
     290
     291        if ( ! is_array( $items ) ) {
     292            return $normalized;
     293        }
     294
     295        foreach ( $items as $item ) {
     296            $item = call_user_func( $sanitizer, $item );
     297
     298            if ( $item ) {
     299                $normalized[] = $item;
     300            }
     301        }
     302
     303        return array_values( array_unique( $normalized ) );
     304    }
     305
     306    /**
     307     * Get sanitized term slugs for a product and taxonomy.
     308     *
     309     * @param int    $product_id Product ID.
     310     * @param string $taxonomy Taxonomy name.
     311     * @return array<int, string>
     312     */
     313    private function get_product_term_slugs( $product_id, $taxonomy ) {
     314        $product_id = absint( $product_id );
     315
     316        if ( $product_id <= 0 ) {
     317            return [];
     318        }
     319
     320        $terms = get_the_terms( $product_id, $taxonomy );
     321
     322        if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
     323            return [];
     324        }
     325
     326        $slugs = [];
     327
     328        foreach ( $terms as $term ) {
     329            if ( isset( $term->slug ) ) {
     330                $slugs[] = sanitize_title( $term->slug );
     331            }
     332        }
     333
     334        return array_values( array_unique( array_filter( $slugs ) ) );
     335    }
    184336}
  • cartflush-autoclear-cart-for-inactive-users/trunk/readme.txt

    r3486304 r3488606  
    1 ## === CartFlush – Auto Clear WooCommerce Cart for Inactive Users ===
     1=== CartFlush - Auto Clear WooCommerce Cart for Inactive Users ===
    22
    33Contributors: wprashed
     
    66Tested up to: 6.8
    77Requires PHP: 7.4
    8 Stable tag: 2.0.0
     8Stable tag: 2.1.0
    99License: GPLv2 or later
    10 License URI: [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html)
     10License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1111
    1212Automatically clear inactive WooCommerce carts with advanced timeout rules, exclusions, and import/export tools.
    1313
    14 ---
    15 
    16 ## == Description ==
     14== Description ==
    1715
    1816CartFlush helps you automatically clear inactive WooCommerce carts, keeping your store clean, fast, and optimized.
    1917
    20 Instead of relying on a basic timeout, CartFlush gives you full control over how cart expiration works. You can define different timeout rules based on user roles, product categories, and exclusions—making it flexible enough for real-world eCommerce scenarios.
     18Instead of relying on a single timeout, CartFlush gives you full control over cart expiration from one settings screen under WooCommerce. You can define different timeout rules based on customer type, user roles, products, categories, and tags, then add exclusions for carts that should never be cleared automatically.
    2119
    2220Whether you want faster cart turnover, better session management, or cleaner abandoned cart handling, CartFlush gives you the tools to do it properly.
     
    2422== Live Demo ==
    2523
    26 🚀 [Try Live Demo (No Setup Required)](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2Fwprashed%2Fcartflush-autoclear-cart-for-inactive-users%2F185b7faf5918e286fe9ca1fb1563d70455d4e095%2Fblueprint.json)
    27 
    28 ---
    29 
    30 ## 🚀 Why Use CartFlush?
     24[Try Live Demo (No Setup Required)](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2Fwprashed%2Fcartflush-autoclear-cart-for-inactive-users%2F185b7faf5918e286fe9ca1fb1563d70455d4e095%2Fblueprint.json)
     25
     26== Why Use CartFlush? ==
    3127
    3228* Prevent stale and abandoned carts from piling up
    3329* Improve WooCommerce session performance
    34 * Apply smarter rules based on users and products
    35 * Save time with import/export configuration tools
    36 * Maintain clean and optimized cart behavior
    37 
    38 ---
    39 
    40 ## == Key Features ==
    41 
    42 ### ⏱ Default Cart Timeout
    43 
    44 Set a global inactivity timeout (in minutes). If no other rules apply, this value determines when a cart is cleared.
    45 
    46 ---
    47 
    48 ### 👤 Role-Based Timeout Rules
    49 
    50 Define custom cart expiration times based on user roles.
     30* Apply smarter rules based on customers and products
     31* Manage rules visually from the WooCommerce settings page
     32* Import or export rules for fast setup and migration
     33
     34== Key Features ==
     35
     36=== Default Cart Timeout ===
     37
     38Set a global inactivity timeout in minutes. If no other rules apply, this value determines when a cart is cleared.
     39
     40=== Visual Rule Builder ===
     41
     42Create and edit cart timeout rules directly from the CartFlush settings page under WooCommerce. No CSV import is required for day-to-day management.
     43
     44=== Customer Type Rules ===
     45
     46Define separate timeouts for:
     47
     48* Guest customers
     49* Logged-in customers
     50
     51=== Role-Based Timeout Rules ===
     52
     53Define custom cart expiration times for specific user roles.
    5154
    5255Examples:
    5356
    54 * Customers → 30 minutes
    55 * Subscribers → 60 minutes
    56 * Wholesale users → 120 minutes
    57 
    58 Perfect for stores with different user types and behaviors.
    59 
    60 ---
    61 
    62 ### 🛍 Category-Based Timeout Rules
    63 
    64 Set cart timeout rules based on product categories.
    65 
    66 Use cases:
    67 
    68 * Flash sale items → shorter timeout
    69 * Subscription products → shorter timeout
    70 * High-value products → longer timeout
    71 
    72 CartFlush checks all items in the cart and applies the most relevant rule.
    73 
    74 ---
    75 
    76 ### ⚡ Smart Timeout Logic
    77 
    78 When multiple rules apply, CartFlush automatically selects the **shortest timeout**.
    79 
    80 This ensures:
    81 
    82 * Predictable behavior
    83 * Better control over urgency
    84 * No rule conflicts
    85 
    86 ---
    87 
    88 ### 🚫 Product Exclusions
    89 
    90 Exclude specific products from cart clearing.
    91 
    92 If a cart contains an excluded product:
    93 → The cart will NOT be cleared.
    94 
    95 ---
    96 
    97 ### 📂 Category Exclusions
    98 
    99 Exclude entire categories from auto-clear.
    100 
    101 If any product in the cart belongs to an excluded category:
    102 → Cart clearing is skipped.
    103 
    104 ---
    105 
    106 ### 📥 CSV Import for Rules
    107 
    108 Bulk import rules using CSV.
    109 
    110 Supported types:
    111 
     57* Customers - 30 minutes
     58* Subscribers - 60 minutes
     59* Wholesale users - 120 minutes
     60
     61=== Product, Category, and Tag Rules ===
     62
     63Apply specific timeout values based on:
     64
     65* Product ID
     66* Product category
     67* Product tag
     68
     69This makes it easy to create shorter or longer expiration windows for special items, campaigns, or collections.
     70
     71=== Smart Timeout Logic ===
     72
     73When multiple timeout rules apply, CartFlush automatically uses the shortest valid timeout.
     74
     75=== Exclusion Rules ===
     76
     77Prevent cart clearing entirely for matching carts using:
     78
     79* Excluded roles
     80* Excluded products
     81* Excluded categories
     82* Excluded tags
     83
     84=== CSV Import for Bulk Rules ===
     85
     86Bulk import rule data with CSV when that is faster than manual entry.
     87
     88Supported CSV types:
     89
     90* customer_type
    11291* role
     92* product_rule
    11393* category
     94* tag
     95* excluded_role
    11496* excluded_product
    11597* excluded_category
    116 
    117 Quickly configure large stores without manual setup.
    118 
    119 ---
    120 
    121 ### 📤 JSON Export (Full Backup)
    122 
    123 Export all settings into a JSON file.
    124 
    125 Includes:
    126 
    127 * Default timeout
    128 * Role rules
    129 * Category rules
    130 * Exclusions
    131 
    132 Perfect for backups and migrations.
    133 
    134 ---
    135 
    136 ### 🔁 JSON Import (Quick Setup)
    137 
    138 Import settings instantly on another site.
    139 
    140 Ideal for:
    141 
    142 * Agencies
    143 * Multi-store setups
    144 * Staging → production deployment
    145 
    146 ---
    147 
    148 ### 👥 Works for Guests & Logged-in Users
    149 
    150 CartFlush uses WooCommerce sessions, so it works for:
    151 
    152 * Guest users
    153 * Logged-in customers
    154 
    155 No additional configuration required.
    156 
    157 ---
    158 
    159 ### 🎯 Lightweight & Efficient
    160 
    161 No unnecessary overhead. The plugin focuses only on:
    162 
    163 * Tracking inactivity
    164 * Applying rules
    165 * Clearing carts
    166 
    167 ---
    168 
    169 ### 🌍 Translation Ready
    170 
    171 Includes text domain and POT file for easy localization.
    172 
    173 ---
    174 
    175 ### 🧹 Clean Uninstall
    176 
    177 When the plugin is deleted:
    178 
    179 * All data and settings are removed automatically
    180 
    181 ---
    182 
    183 ## == How It Works ==
    184 
    185 1. Customer adds items to cart
    186 2. Inactivity timer starts
    187 3. Plugin checks:
    188 
    189    * Default timeout
    190    * User role rules
    191    * Product category rules
    192 4. Shortest valid timeout is applied
    193 5. If excluded items exist → skip clearing
    194 6. Cart is cleared after timeout
    195 
    196 ---
    197 
    198 ## == Supported Import Formats ==
     98* excluded_tag
     99
     100=== JSON Import and Export ===
     101
     102Export the full CartFlush configuration as JSON for backup or migration, then import it on another store when needed.
     103
     104=== WooCommerce Menu Integration ===
     105
     106The CartFlush settings page is available directly under the WooCommerce admin menu for quicker access.
     107
     108=== Lightweight and Efficient ===
     109
     110CartFlush focuses only on inactivity tracking, rule evaluation, and cart clearing without adding unnecessary overhead.
     111
     112=== Translation Ready ===
     113
     114Includes the `cartflush` text domain for localization.
     115
     116=== Clean Uninstall ===
     117
     118When the plugin is deleted, CartFlush removes its stored settings automatically.
     119
     120== How It Works ==
     121
     1221. A customer adds items to the cart.
     1232. The inactivity timer begins.
     1243. CartFlush checks the default timeout and any matching timeout rules.
     1254. The shortest valid timeout is selected.
     1265. If an exclusion rule matches, cart clearing is skipped.
     1276. The cart is cleared after the final timeout is reached.
     128
     129== Supported Import Formats ==
    199130
    200131CSV headers:
     
    202133`type,key,timeout_minutes`
    203134
    204 ### Supported types:
    205 
     135Supported types:
     136
     137* customer_type
    206138* role
     139* product_rule
    207140* category
     141* tag
     142* excluded_role
    208143* excluded_product
    209144* excluded_category
    210 
    211 ### Example:
    212 
     145* excluded_tag
     146
     147Example rows:
     148
     149`customer_type,guest,20`
    213150`role,customer,30`
    214 `category,subscription-box,10`
     151`product_rule,321,10`
     152`category,flash-sale,15`
     153`tag,seasonal,25`
     154`excluded_role,wholesale_customer,`
    215155`excluded_product,123,`
    216156`excluded_category,high-ticket,`
    217 
    218 ---
    219 
    220 ## == Frequently Asked Questions ==
    221 
    222 ### Does this work for guest users and logged-in users?
     157`excluded_tag,fragile,`
     158
     159== Frequently Asked Questions ==
     160
     161=== Does this work for guest users and logged-in users? ===
    223162
    224163Yes. CartFlush uses WooCommerce sessions, so both are supported.
    225164
    226 ---
    227 
    228 ### How is the timeout calculated?
    229 
    230 The plugin starts with the default timeout, then checks role and category rules. The shortest valid timeout is applied.
    231 
    232 ---
    233 
    234 ### What if a cart contains excluded items?
    235 
    236 CartFlush will skip clearing the cart entirely.
    237 
    238 ---
    239 
    240 ### Can I migrate settings between sites?
     165=== How is the timeout calculated? ===
     166
     167The plugin starts with the default timeout, then checks matching customer type, role, product, category, and tag rules. The shortest valid timeout is applied.
     168
     169=== What if a cart contains excluded items? ===
     170
     171CartFlush skips clearing the cart entirely.
     172
     173=== Can I manage rules without importing CSV? ===
     174
     175Yes. Rules can be added and edited directly from the CartFlush settings page.
     176
     177=== Can I migrate settings between sites? ===
    241178
    242179Yes. Export settings as JSON and import them on another site.
    243180
    244 ---
    245 
    246 ### Does uninstall remove all data?
     181=== Does uninstall remove all data? ===
    247182
    248183Yes. All plugin options are deleted during uninstall.
    249184
    250 ---
    251 
    252 ## == Screenshots ==
    253 
    254 1. Clean and modern CartFlush settings panel
    255 2. CSV import interface for rules
    256 3. JSON export/import tools
    257 4. Active rules and exclusions overview
    258 
    259 ---
    260 
    261 ## == Changelog ==
     185== Screenshots ==
     186
     1871. Modern CartFlush settings page under WooCommerce
     1882. Manual rule builder for timeout rules, CSV and JSON import/export tools
     1893. Exclusion Rules
     1904. Saved configuration overview
     191
     192== Changelog ==
     193
     194= 2.1.0 =
     195
     196* Added a full visual rule builder to the settings page
     197* Moved the plugin page under the WooCommerce admin menu
     198* Added customer type, product, and tag timeout rules
     199* Added excluded role and excluded tag support
     200* Expanded CSV import to support all new rule types
     201* Redesigned the admin settings interface with a more modern layout
     202* Improved import/export presentation and rule card usability
    262203
    263204= 2.0.0 =
Note: See TracChangeset for help on using the changeset viewer.