Changeset 3744939
- Timestamp:
- 10/13/2026 09:38:19 PM (10 days ago)
- Location:
- alnora-clinic
- Files:
-
- 14 edited
- 25 copied
-
tags/1.3.7 (copied) (copied from alnora-clinic/trunk )
-
tags/1.3.7/admin/class-admin.php (copied) (copied from alnora-clinic/trunk/admin/class-admin.php ) (4 diffs)
-
tags/1.3.7/admin/css/alnc-admin.css (modified) (1 diff)
-
tags/1.3.7/admin/partials/_notice.php (copied) (copied from alnora-clinic/trunk/admin/partials/_notice.php )
-
tags/1.3.7/admin/partials/settings.php (copied) (copied from alnora-clinic/trunk/admin/partials/settings.php ) (4 diffs)
-
tags/1.3.7/alnora-clinic.php (copied) (copied from alnora-clinic/trunk/alnora-clinic.php ) (2 diffs)
-
tags/1.3.7/api/class-rest-routes.php (modified) (1 diff)
-
tags/1.3.7/assets/css/alnc-brand.css (copied) (copied from alnora-clinic/trunk/assets/css/alnc-brand.css )
-
tags/1.3.7/frontend/class-frontend.php (copied) (copied from alnora-clinic/trunk/frontend/class-frontend.php ) (2 diffs)
-
tags/1.3.7/frontend/js/alnc-booking.js (copied) (copied from alnora-clinic/trunk/frontend/js/alnc-booking.js ) (3 diffs)
-
tags/1.3.7/includes/class-activator.php (copied) (copied from alnora-clinic/trunk/includes/class-activator.php )
-
tags/1.3.7/includes/class-appointment-manager.php (modified) (3 diffs)
-
tags/1.3.7/includes/class-encryption.php (copied) (copied from alnora-clinic/trunk/includes/class-encryption.php )
-
tags/1.3.7/includes/class-loader.php (copied) (copied from alnora-clinic/trunk/includes/class-loader.php )
-
tags/1.3.7/includes/class-patient-manager.php (copied) (copied from alnora-clinic/trunk/includes/class-patient-manager.php )
-
tags/1.3.7/includes/class-settings.php (modified) (1 diff)
-
tags/1.3.7/readme.txt (copied) (copied from alnora-clinic/trunk/readme.txt ) (4 diffs)
-
tags/1.3.7/vendor/freemius/assets/js/pricing/freemius-pricing.js (copied) (copied from alnora-clinic/trunk/vendor/freemius/assets/js/pricing/freemius-pricing.js )
-
tags/1.3.7/vendor/freemius/composer.json (copied) (copied from alnora-clinic/trunk/vendor/freemius/composer.json )
-
tags/1.3.7/vendor/freemius/config.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/config.php )
-
tags/1.3.7/vendor/freemius/includes/class-freemius.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/class-freemius.php )
-
tags/1.3.7/vendor/freemius/includes/entities/class-fs-payment.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/entities/class-fs-payment.php )
-
tags/1.3.7/vendor/freemius/includes/managers/class-fs-checkout-manager.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/managers/class-fs-checkout-manager.php )
-
tags/1.3.7/vendor/freemius/includes/managers/class-fs-clone-manager.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/managers/class-fs-clone-manager.php )
-
tags/1.3.7/vendor/freemius/includes/managers/class-fs-contact-form-manager.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/managers/class-fs-contact-form-manager.php )
-
tags/1.3.7/vendor/freemius/includes/sdk/FreemiusWordPress.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/includes/sdk/FreemiusWordPress.php )
-
tags/1.3.7/vendor/freemius/start.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/start.php )
-
tags/1.3.7/vendor/freemius/templates/add-ons.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/templates/add-ons.php )
-
tags/1.3.7/vendor/freemius/templates/pricing.php (copied) (copied from alnora-clinic/trunk/vendor/freemius/templates/pricing.php )
-
trunk/admin/class-admin.php (modified) (4 diffs)
-
trunk/admin/css/alnc-admin.css (modified) (1 diff)
-
trunk/admin/partials/settings.php (modified) (4 diffs)
-
trunk/alnora-clinic.php (modified) (2 diffs)
-
trunk/api/class-rest-routes.php (modified) (1 diff)
-
trunk/frontend/class-frontend.php (modified) (2 diffs)
-
trunk/frontend/js/alnc-booking.js (modified) (3 diffs)
-
trunk/includes/class-appointment-manager.php (modified) (3 diffs)
-
trunk/includes/class-settings.php (modified) (1 diff)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
alnora-clinic/tags/1.3.7/admin/class-admin.php
r3703472 r3744939 96 96 add_submenu_page( self::SLUG, __( 'Shortcodes', 'alnora-clinic' ), __( 'Shortcodes', 'alnora-clinic' ), $cap, self::SLUG . '-settings-shortcodes', array( $this, 'render_settings' ) ); 97 97 add_submenu_page( self::SLUG, __( 'Booking rules', 'alnora-clinic' ), __( 'Booking rules', 'alnora-clinic' ), $cap, self::SLUG . '-settings-booking', array( $this, 'render_settings' ) ); 98 add_submenu_page( self::SLUG, __( 'Form', 'alnora-clinic' ), __( 'Form', 'alnora-clinic' ), $cap, self::SLUG . '-settings-form', array( $this, 'render_settings' ) ); 98 99 add_submenu_page( self::SLUG, __( 'Email & reminders', 'alnora-clinic' ), __( 'Email & reminders', 'alnora-clinic' ), $cap, self::SLUG . '-settings-email', array( $this, 'render_settings' ) ); 99 100 add_submenu_page( self::SLUG, __( 'GDPR', 'alnora-clinic' ), __( 'GDPR', 'alnora-clinic' ), $cap, self::SLUG . '-settings-gdpr', array( $this, 'render_settings' ) ); … … 112 113 $css = '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-shortcodes"]),' 113 114 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-booking"]),' 115 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-form"]),' 114 116 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-email"]),' 115 117 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-gdpr"]){display:none !important;}'; … … 614 616 self::SLUG . '-settings-shortcodes' => 'shortcodes', 615 617 self::SLUG . '-settings-booking' => 'booking', 618 self::SLUG . '-settings-form' => 'form', 616 619 self::SLUG . '-settings-email' => 'email', 617 620 self::SLUG . '-settings-gdpr' => 'gdpr', … … 834 837 } 835 838 839 // Core booking-form fields (Settings → Form). Same keys as Pro. 840 foreach ( array( 841 'form_field_name_label', 'form_field_email_label', 'form_field_phone_label', 'form_field_reason_label', 842 'form_field_name_placeholder', 'form_field_email_placeholder', 'form_field_phone_placeholder', 'form_field_reason_placeholder', 843 ) as $key ) { 844 if ( array_key_exists( $key, $post ) ) { 845 $out[ $key ] = sanitize_text_field( (string) $post[ $key ] ); 846 } 847 } 848 foreach ( array( 'form_field_phone_enabled', 'form_field_phone_required', 'form_field_reason_enabled', 'form_field_reason_required' ) as $key ) { 849 if ( array_key_exists( $key, $post ) ) { 850 $out[ $key ] = (int) (bool) $post[ $key ]; 851 } 852 } 853 836 854 if ( array_key_exists( 'delete_data_on_uninstall', $post ) ) { 837 855 $out['delete_data_on_uninstall'] = (int) (bool) $post['delete_data_on_uninstall']; -
alnora-clinic/tags/1.3.7/admin/css/alnc-admin.css
r3668931 r3744939 702 702 } 703 703 } 704 705 /* ── Settings → Form: core field rows (same look as Pro, without the Rules column) ── */ 706 .alnc-form-fields-table { 707 display: flex; 708 flex-direction: column; 709 border: 1px solid var(--alnc-border); 710 border-radius: var(--alnc-radius-md); 711 overflow: hidden; 712 } 713 .alnc-form-fields-table__head, 714 .alnc-form-fields-table__row { 715 display: grid; 716 grid-template-columns: 100px 1fr 1fr 90px 90px; 717 gap: var(--alnc-space-4); 718 align-items: center; 719 padding: var(--alnc-space-3) var(--alnc-space-4); 720 } 721 .alnc-form-fields-table__head { 722 background: var(--alnc-teal-mist); 723 font-size: var(--alnc-text-xs); 724 font-weight: var(--alnc-weight-semibold); 725 color: var(--alnc-text-muted); 726 text-transform: uppercase; 727 letter-spacing: 0.04em; 728 } 729 .alnc-form-fields-table__row { border-top: 1px solid var(--alnc-border-subtle); } 730 .alnc-form-fields-table__row:first-of-type { border-top: none; } 731 .alnc-form-fields-table__toggle { display: flex; align-items: center; } 732 @media (max-width: 700px) { 733 .alnc-form-fields-table__head { display: none; } 734 .alnc-form-fields-table__row { grid-template-columns: 1fr 1fr; } 735 .alnc-form-fields-table__row .alnc-badge { grid-column: 1 / -1; } 736 } -
alnora-clinic/tags/1.3.7/admin/partials/settings.php
r3707811 r3744939 6 6 * @var array $settings 7 7 * @var string $booking_url 8 * @var string $alnc_tab Active tab: general|shortcodes|booking| email|gdpr8 * @var string $alnc_tab Active tab: general|shortcodes|booking|form|email|gdpr 9 9 */ 10 10 … … 19 19 'shortcodes' => 'alnora-clinic-settings-shortcodes', 20 20 'booking' => 'alnora-clinic-settings-booking', 21 'form' => 'alnora-clinic-settings-form', 21 22 'email' => 'alnora-clinic-settings-email', 22 23 'gdpr' => 'alnora-clinic-settings-gdpr', … … 36 37 'shortcodes' => __( 'Shortcodes', 'alnora-clinic' ), 37 38 'booking' => __( 'Booking rules', 'alnora-clinic' ), 39 'form' => __( 'Form', 'alnora-clinic' ), 38 40 'email' => __( 'Email & reminders', 'alnora-clinic' ), 39 41 'gdpr' => __( 'GDPR', 'alnora-clinic' ), … … 167 169 </div> 168 170 171 <?php /* ── FORM ─────────────────────────────────────────────────── */ ?> 172 <?php elseif ( 'form' === $alnc_tab ) : ?> 173 174 <div class="alnc-card"> 175 <h2 class="alnc-h2"><?php esc_html_e( 'Core fields', 'alnora-clinic' ); ?></h2> 176 <p style="color:var(--alnc-text-muted);margin-bottom:var(--alnc-space-5)"><?php esc_html_e( 'Customize labels, visibility and required status for each field on the booking form.', 'alnora-clinic' ); ?></p> 177 178 <?php 179 /* The same rows and setting keys as Pro's Form tab, so everything set here carries 180 * over on upgrade. Name and email are always on and always required: a booking 181 * without them cannot become a patient record. */ 182 $alnc_core_fields = array( 183 array( 'key' => 'name', 'label_key' => 'form_field_name_label', 'default' => 'Full name', 'always' => true ), 184 array( 'key' => 'email', 'label_key' => 'form_field_email_label', 'default' => 'Email', 'always' => true ), 185 array( 'key' => 'phone', 'label_key' => 'form_field_phone_label', 'default' => 'Phone', 'always' => false ), 186 array( 'key' => 'reason', 'label_key' => 'form_field_reason_label', 'default' => 'Reason for visit (optional)', 'always' => false ), 187 ); 188 ?> 189 190 <div class="alnc-form-fields-table"> 191 <div class="alnc-form-fields-table__head"> 192 <span><?php esc_html_e( 'Field', 'alnora-clinic' ); ?></span> 193 <span><?php esc_html_e( 'Label', 'alnora-clinic' ); ?></span> 194 <span><?php esc_html_e( 'Placeholder', 'alnora-clinic' ); ?></span> 195 <span><?php esc_html_e( 'Required', 'alnora-clinic' ); ?></span> 196 <span><?php esc_html_e( 'Enabled', 'alnora-clinic' ); ?></span> 197 </div> 198 199 <?php foreach ( $alnc_core_fields as $alnc_cf ) : ?> 200 <?php 201 $alnc_ph_key = 'form_field_' . $alnc_cf['key'] . '_placeholder'; 202 $alnc_req_key = 'form_field_' . $alnc_cf['key'] . '_required'; 203 $alnc_en_key = 'form_field_' . $alnc_cf['key'] . '_enabled'; 204 ?> 205 <div class="alnc-form-fields-table__row"> 206 <span class="alnc-badge alnc-badge-navy"><?php echo esc_html( $alnc_cf['key'] ); ?></span> 207 <div class="alnc-field" style="margin:0"> 208 <input class="alnc-input" type="text" 209 name="<?php echo esc_attr( $alnc_cf['label_key'] ); ?>" 210 value="<?php echo esc_attr( (string) ( $settings[ $alnc_cf['label_key'] ] ?? $alnc_cf['default'] ) ); ?>" 211 placeholder="<?php echo esc_attr( $alnc_cf['default'] ); ?>" 212 aria-label="<?php echo esc_attr( sprintf( /* translators: %s: field key, e.g. phone. */ __( 'Label for %s', 'alnora-clinic' ), $alnc_cf['key'] ) ); ?>"> 213 </div> 214 <div class="alnc-field" style="margin:0"> 215 <input class="alnc-input" type="text" 216 name="<?php echo esc_attr( $alnc_ph_key ); ?>" 217 value="<?php echo esc_attr( (string) ( $settings[ $alnc_ph_key ] ?? '' ) ); ?>" 218 placeholder="<?php esc_attr_e( 'Shown inside the empty field', 'alnora-clinic' ); ?>" 219 aria-label="<?php echo esc_attr( sprintf( /* translators: %s: field key, e.g. phone. */ __( 'Placeholder for %s', 'alnora-clinic' ), $alnc_cf['key'] ) ); ?>"> 220 </div> 221 <div class="alnc-form-fields-table__toggle"> 222 <?php if ( $alnc_cf['always'] ) : ?> 223 <span class="alnc-badge alnc-badge-teal"><?php esc_html_e( 'Always', 'alnora-clinic' ); ?></span> 224 <?php else : ?> 225 <input type="hidden" name="<?php echo esc_attr( $alnc_req_key ); ?>" value="0"> 226 <label class="alnc-switch"> 227 <input type="checkbox" name="<?php echo esc_attr( $alnc_req_key ); ?>" value="1" <?php checked( ! empty( $settings[ $alnc_req_key ] ) ); ?>> 228 <span></span> 229 </label> 230 <?php endif; ?> 231 </div> 232 <div class="alnc-form-fields-table__toggle"> 233 <?php if ( $alnc_cf['always'] ) : ?> 234 <span class="alnc-badge alnc-badge-teal"><?php esc_html_e( 'Always', 'alnora-clinic' ); ?></span> 235 <?php else : ?> 236 <input type="hidden" name="<?php echo esc_attr( $alnc_en_key ); ?>" value="0"> 237 <label class="alnc-switch"> 238 <input type="checkbox" name="<?php echo esc_attr( $alnc_en_key ); ?>" value="1" <?php checked( ! empty( $settings[ $alnc_en_key ] ) ); ?>> 239 <span></span> 240 </label> 241 <?php endif; ?> 242 </div> 243 </div> 244 <?php endforeach; ?> 245 </div> 246 247 <p class="alnc-field-hint" style="margin-top:var(--alnc-space-4)"><?php esc_html_e( 'In the Pro version you can add your own questions, show a field only for certain doctors or services, split the name into name and surname,', 'alnora-clinic' ); ?> <a href="https://alnora-booking.com/docs/#booking-form-fields" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'and much more', 'alnora-clinic' ); ?></a>.</p> 248 </div> 249 169 250 <?php /* ── EMAIL & REMINDERS ──────────────────────────────────── */ ?> 170 251 <?php elseif ( 'email' === $alnc_tab ) : ?> -
alnora-clinic/tags/1.3.7/alnora-clinic.php
r3742445 r3744939 6 6 * Plugin URI: https://alnora-booking.com 7 7 * Description: Appointment booking for private medical clinics — doctors, services, schedules, patient records and automated email notifications. 8 * Version: 1.3. 68 * Version: 1.3.7 9 9 * Requires at least: 6.0 10 10 * Requires PHP: 7.4 … … 62 62 |-------------------------------------------------------------------------- 63 63 */ 64 if ( ! defined( 'ALNC_VERSION' ) ) { define( 'ALNC_VERSION', '1.3. 6' ); }64 if ( ! defined( 'ALNC_VERSION' ) ) { define( 'ALNC_VERSION', '1.3.7' ); } 65 65 if ( ! defined( 'ALNC_DB_VERSION' ) ) { define( 'ALNC_DB_VERSION', '1.7.2' ); } 66 66 if ( ! defined( 'ALNC_FILE' ) ) { define( 'ALNC_FILE', __FILE__ ); } -
alnora-clinic/tags/1.3.7/api/class-rest-routes.php
r3604289 r3744939 359 359 } 360 360 361 $result = Appointment_Manager::create_booking( (array) $params ); 361 // Settings → Form: a field switched off is never stored, and a required one must 362 // be filled. Checked here as well as in the form, so the rule holds for any client. 363 $params = (array) $params; 364 foreach ( array( 'phone' => 'patient_phone', 'reason' => 'reason' ) as $field => $param ) { 365 $conf = \AlnoraClinic\Frontend\Frontend::core_fields()[ $field ]; 366 if ( ! $conf['enabled'] ) { 367 unset( $params[ $param ] ); 368 } elseif ( $conf['required'] && '' === trim( (string) ( $params[ $param ] ?? '' ) ) ) { 369 // In the patient language, like the rest of the form. 370 $strings = \AlnoraClinic\Translations::get( (string) \AlnoraClinic\Settings::get( 'language', 'en_US' ) ); 371 return new \WP_Error( 'alnc_missing_field', (string) ( $strings['required'] ?? __( 'Please fill in all required fields.', 'alnora-clinic' ) ), array( 'status' => 400 ) ); 372 } 373 } 374 375 $result = Appointment_Manager::create_booking( $params ); 362 376 363 377 if ( is_wp_error( $result ) ) { -
alnora-clinic/tags/1.3.7/frontend/class-frontend.php
r3687687 r3744939 105 105 106 106 /** 107 * The core booking fields as set under Settings → Form. 108 * 109 * A label still at its English default is sent empty, so the form shows its own 110 * translation in the patient language; only a label the clinic changed is sent as 111 * typed. Name and email are always shown and always required. 112 * 113 * @return array<string,array<string,mixed>> 114 */ 115 public static function core_fields(): array { 116 $defaults = array( 117 'name' => 'Full name', 118 'email' => 'Email', 119 'phone' => 'Phone', 120 'reason' => 'Reason for visit (optional)', 121 ); 122 $fields = array(); 123 foreach ( $defaults as $key => $default ) { 124 $label = trim( (string) Settings::get( 'form_field_' . $key . '_label', $default ) ); 125 $fixed = in_array( $key, array( 'name', 'email' ), true ); 126 127 $fields[ $key ] = array( 128 'label' => $label === $default ? '' : $label, 129 'placeholder' => (string) Settings::get( 'form_field_' . $key . '_placeholder', '' ), 130 'enabled' => $fixed || (bool) Settings::get( 'form_field_' . $key . '_enabled', 1 ), 131 'required' => $fixed || (bool) Settings::get( 'form_field_' . $key . '_required', 0 ), 132 ); 133 } 134 return $fields; 135 } 136 137 /** 107 138 * Shortcode handler. 108 139 * … … 156 187 'timeFormat' => (string) get_option( 'time_format', 'g:i a' ), 157 188 'i18n' => \AlnoraClinic\Translations::get( $lang_code ), 189 'fields' => self::core_fields(), 158 190 ); 159 191 -
alnora-clinic/tags/1.3.7/frontend/js/alnc-booking.js
r3687687 r3744939 483 483 ] ); 484 484 485 // Core fields as set under Settings → Form. An empty label means "still the 486 // default", so the patient-language translation is shown instead. 487 var F = CFG.fields || {}; 488 function conf( key ) { 489 var c = F[ key ] || {}; 490 var fixed = ( 'name' === key || 'email' === key ); 491 var label = c.label || T[ key ] || key; 492 // The default reason label ends in "(optional)" in every language; drop it 493 // once the clinic makes the field required. 494 if ( ! c.label && c.required ) { label = label.replace( /\s*\([^)]*\)\s*$/, '' ); } 495 return { 496 label: label + ( ( fixed || c.required ) ? ' *' : '' ), 497 placeholder: c.placeholder || '', 498 enabled: fixed || c.enabled !== false, 499 required: fixed || !! c.required 500 }; 501 } 502 var cName = conf( 'name' ), cEmail = conf( 'email' ), cPhone = conf( 'phone' ), cReason = conf( 'reason' ); 503 485 504 var form = el( 'form', { class: 'alnc-form', autocomplete: 'on' } ); 486 form.appendChild( field( 'alnc-f-name', T.name + ' *', 'text', 'name', true ) ); 487 form.appendChild( el( 'div', { class: 'alnc-form__row' }, [ 488 field( 'alnc-f-email', T.email + ' *', 'email', 'email', true ), 489 field( 'alnc-f-phone', T.phone, 'tel', 'phone', false ) 490 ] ) ); 491 form.appendChild( textareaField( 'alnc-f-reason', T.reason, 'reason' ) ); 505 form.appendChild( field( 'alnc-f-name', cName.label, 'text', 'name', true, cName.placeholder ) ); 506 var emailField = field( 'alnc-f-email', cEmail.label, 'email', 'email', true, cEmail.placeholder ); 507 if ( cPhone.enabled ) { 508 // Email and phone share a two-column row. 509 form.appendChild( el( 'div', { class: 'alnc-form__row' }, [ 510 emailField, 511 field( 'alnc-f-phone', cPhone.label, 'tel', 'tel', cPhone.required, cPhone.placeholder ) 512 ] ) ); 513 } else { 514 // No phone: email takes the full width, like the name above it. 515 form.appendChild( emailField ); 516 } 517 if ( cReason.enabled ) { 518 form.appendChild( textareaField( 'alnc-f-reason', cReason.label, 'reason', cReason.required, cReason.placeholder ) ); 519 } 492 520 493 521 // Recurring (Pro): optional repeat controls. … … 527 555 var name = form.querySelector( '#alnc-f-name' ).value.trim(); 528 556 var email = form.querySelector( '#alnc-f-email' ).value.trim(); 529 var phone = form.querySelector( '#alnc-f-phone' ).value.trim(); 530 var reason = form.querySelector( '#alnc-f-reason' ).value.trim(); 557 var phoneEl = form.querySelector( '#alnc-f-phone' ); 558 var reasonEl = form.querySelector( '#alnc-f-reason' ); 559 var phone = phoneEl ? phoneEl.value.trim() : ''; 560 var reason = reasonEl ? reasonEl.value.trim() : ''; 531 561 var consented = form.querySelector( '#' + consentId ).checked; 532 562 533 563 if ( ! name || ! email ) { return showErr( T.required ); } 564 if ( ( phoneEl && cPhone.required && ! phone ) || ( reasonEl && cReason.required && ! reason ) ) { return showErr( T.required ); } 534 565 if ( ! consented ) { return showErr( T.consentReq ); } 535 566 … … 696 727 697 728 /* ----- field factories ----- */ 698 function field( id, label, type, name, required ) {729 function field( id, label, type, name, required, placeholder ) { 699 730 return el( 'div', { class: 'alnc-field' }, [ 700 731 el( 'label', { class: 'alnc-field-label', for: id, text: label } ), 701 el( 'input', { class: 'alnc-input', id: id, name: name, type: type, required: required ? 'required' : null, autocomplete: name } )732 el( 'input', { class: 'alnc-input', id: id, name: name, type: type, required: required ? 'required' : null, placeholder: placeholder || null, autocomplete: name } ) 702 733 ] ); 703 734 } 704 function textareaField( id, label, name ) {735 function textareaField( id, label, name, required, placeholder ) { 705 736 return el( 'div', { class: 'alnc-field' }, [ 706 737 el( 'label', { class: 'alnc-field-label', for: id, text: label } ), 707 el( 'textarea', { class: 'alnc-textarea', id: id, name: name, rows: '3' } )738 el( 'textarea', { class: 'alnc-textarea', id: id, name: name, rows: '3', required: required ? 'required' : null, placeholder: placeholder || null } ) 708 739 ] ); 709 740 } -
alnora-clinic/tags/1.3.7/includes/class-appointment-manager.php
r3585842 r3744939 385 385 386 386 /** 387 * Generate a unique reference number, e.g. alnc-2026-0042. 387 * Generate a unique reference number, e.g. 26-0042. 388 * 389 * Two-digit year, the same as Pro, so a clinic moving between the two keeps one 390 * numbering style. References issued earlier as 2026-0042 stay as they are: they 391 * cannot collide with the new 26- series, which is counted on its own. 388 392 * 389 393 * @return string … … 392 396 global $wpdb; 393 397 $table = self::table(); 394 $year = current_time( 'Y' );398 $year = (string) current_time( 'y' ); 395 399 396 400 // Next sequence = highest existing sequence for this year + 1. Using MAX (not … … 399 403 // failing the insert ("Could not save the appointment"). 400 404 $like = $wpdb->esc_like( $year . '-' ) . '%'; 401 $max = (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX( CAST( SUBSTRING_INDEX( ref_number, '-', -1 ) AS UNSIGNED )) FROM {$table} WHERE ref_number LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared405 $max = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE( MAX( CAST( SUBSTRING_INDEX( ref_number, '-', -1 ) AS UNSIGNED ) ), 0 ) FROM {$table} WHERE ref_number LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 402 406 $seq = $max + 1; 403 407 -
alnora-clinic/tags/1.3.7/includes/class-settings.php
r3657434 r3744939 44 44 // a reinstall. Turn on only if you want everything erased on uninstall. 45 45 'delete_data_on_uninstall' => 0, 46 // Core booking-form fields (Settings → Form). Same keys and defaults as Pro, 47 // so a clinic's labels and switches carry over unchanged on upgrade. 48 'form_field_name_label' => 'Full name', 49 'form_field_email_label' => 'Email', 50 'form_field_phone_label' => 'Phone', 51 'form_field_phone_enabled' => 1, 52 'form_field_phone_required' => 0, 53 'form_field_reason_label' => 'Reason for visit (optional)', 54 'form_field_reason_enabled' => 1, 55 'form_field_reason_required' => 0, 56 'form_field_name_placeholder' => '', 57 'form_field_email_placeholder' => '', 58 'form_field_phone_placeholder' => '+1 212 555 0147', 59 'form_field_reason_placeholder' => '', 46 60 ); 47 61 } -
alnora-clinic/tags/1.3.7/readme.txt
r3742445 r3744939 5 5 Tested up to: 7.1.2 6 6 Requires PHP: 7.4 7 Stable tag: 1.3. 67 Stable tag: 1.3.7 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 19 19 No generic salon plugin bent into shape. No spreadsheets. No five add-ons duct-taped together. Just one focused product that understands doctors, services, patients and privacy. 20 20 21 👉 [Live demo](https://alnora-booking.com/demo/) · [See Pro pricing](https://alnora-booking.com/pricing/) · [Documentation](https://alnora-booking.com/docs/) 21 👉 [Live demo](https://alnora-booking.com/demo/) · [See Pro pricing](https://alnora-booking.com/pricing/) · [Documentation](https://alnora-booking.com/docs/) · [YouTube](https://www.youtube.com/@alnora-clinic) 22 22 23 23 **Why private clinics choose Alnora** … … 150 150 == Changelog == 151 151 152 = 1.3.7 = 153 * New: **Settings → Form.** Change the label and placeholder of each field on the booking form: name, email, phone and reason for visit. Phone and reason each have their own **Required** and **Enabled** switches, so you can ask for a phone number on every booking or leave the reason box off the form entirely. Name and email are always shown and always required. A label you leave as it is still shows in the patient's language. When phone is switched off, the email field takes the full width. 154 * New: a required field is marked with an asterisk and checked when the booking is sent, as well as in the form. A field you switch off is never saved, even if something else sends it. 155 * Improvement: **shorter appointment references.** New bookings get references such as 26-0042 instead of 2026-0042, the same style as Pro, so a clinic moving between the two keeps one numbering style. References already issued stay as they are. 156 152 157 = 1.3.6 = 153 158 * Updated Freemius SDK to the latest version. … … 257 262 == Upgrade Notice == 258 263 264 = 1.3.7 = 265 New Settings → Form tab to relabel the booking form's fields and make phone or reason required or hidden. New appointment references use the shorter 26-0042 style. Existing references are unchanged. No database changes. 266 259 267 = 1.3.6 = 260 268 Updated Freemius SDK to the latest version. No database or configuration changes. -
alnora-clinic/trunk/admin/class-admin.php
r3703472 r3744939 96 96 add_submenu_page( self::SLUG, __( 'Shortcodes', 'alnora-clinic' ), __( 'Shortcodes', 'alnora-clinic' ), $cap, self::SLUG . '-settings-shortcodes', array( $this, 'render_settings' ) ); 97 97 add_submenu_page( self::SLUG, __( 'Booking rules', 'alnora-clinic' ), __( 'Booking rules', 'alnora-clinic' ), $cap, self::SLUG . '-settings-booking', array( $this, 'render_settings' ) ); 98 add_submenu_page( self::SLUG, __( 'Form', 'alnora-clinic' ), __( 'Form', 'alnora-clinic' ), $cap, self::SLUG . '-settings-form', array( $this, 'render_settings' ) ); 98 99 add_submenu_page( self::SLUG, __( 'Email & reminders', 'alnora-clinic' ), __( 'Email & reminders', 'alnora-clinic' ), $cap, self::SLUG . '-settings-email', array( $this, 'render_settings' ) ); 99 100 add_submenu_page( self::SLUG, __( 'GDPR', 'alnora-clinic' ), __( 'GDPR', 'alnora-clinic' ), $cap, self::SLUG . '-settings-gdpr', array( $this, 'render_settings' ) ); … … 112 113 $css = '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-shortcodes"]),' 113 114 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-booking"]),' 115 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-form"]),' 114 116 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-email"]),' 115 117 . '#adminmenu li:has(> a[href$="page=alnora-clinic-settings-gdpr"]){display:none !important;}'; … … 614 616 self::SLUG . '-settings-shortcodes' => 'shortcodes', 615 617 self::SLUG . '-settings-booking' => 'booking', 618 self::SLUG . '-settings-form' => 'form', 616 619 self::SLUG . '-settings-email' => 'email', 617 620 self::SLUG . '-settings-gdpr' => 'gdpr', … … 834 837 } 835 838 839 // Core booking-form fields (Settings → Form). Same keys as Pro. 840 foreach ( array( 841 'form_field_name_label', 'form_field_email_label', 'form_field_phone_label', 'form_field_reason_label', 842 'form_field_name_placeholder', 'form_field_email_placeholder', 'form_field_phone_placeholder', 'form_field_reason_placeholder', 843 ) as $key ) { 844 if ( array_key_exists( $key, $post ) ) { 845 $out[ $key ] = sanitize_text_field( (string) $post[ $key ] ); 846 } 847 } 848 foreach ( array( 'form_field_phone_enabled', 'form_field_phone_required', 'form_field_reason_enabled', 'form_field_reason_required' ) as $key ) { 849 if ( array_key_exists( $key, $post ) ) { 850 $out[ $key ] = (int) (bool) $post[ $key ]; 851 } 852 } 853 836 854 if ( array_key_exists( 'delete_data_on_uninstall', $post ) ) { 837 855 $out['delete_data_on_uninstall'] = (int) (bool) $post['delete_data_on_uninstall']; -
alnora-clinic/trunk/admin/css/alnc-admin.css
r3668931 r3744939 702 702 } 703 703 } 704 705 /* ── Settings → Form: core field rows (same look as Pro, without the Rules column) ── */ 706 .alnc-form-fields-table { 707 display: flex; 708 flex-direction: column; 709 border: 1px solid var(--alnc-border); 710 border-radius: var(--alnc-radius-md); 711 overflow: hidden; 712 } 713 .alnc-form-fields-table__head, 714 .alnc-form-fields-table__row { 715 display: grid; 716 grid-template-columns: 100px 1fr 1fr 90px 90px; 717 gap: var(--alnc-space-4); 718 align-items: center; 719 padding: var(--alnc-space-3) var(--alnc-space-4); 720 } 721 .alnc-form-fields-table__head { 722 background: var(--alnc-teal-mist); 723 font-size: var(--alnc-text-xs); 724 font-weight: var(--alnc-weight-semibold); 725 color: var(--alnc-text-muted); 726 text-transform: uppercase; 727 letter-spacing: 0.04em; 728 } 729 .alnc-form-fields-table__row { border-top: 1px solid var(--alnc-border-subtle); } 730 .alnc-form-fields-table__row:first-of-type { border-top: none; } 731 .alnc-form-fields-table__toggle { display: flex; align-items: center; } 732 @media (max-width: 700px) { 733 .alnc-form-fields-table__head { display: none; } 734 .alnc-form-fields-table__row { grid-template-columns: 1fr 1fr; } 735 .alnc-form-fields-table__row .alnc-badge { grid-column: 1 / -1; } 736 } -
alnora-clinic/trunk/admin/partials/settings.php
r3707811 r3744939 6 6 * @var array $settings 7 7 * @var string $booking_url 8 * @var string $alnc_tab Active tab: general|shortcodes|booking| email|gdpr8 * @var string $alnc_tab Active tab: general|shortcodes|booking|form|email|gdpr 9 9 */ 10 10 … … 19 19 'shortcodes' => 'alnora-clinic-settings-shortcodes', 20 20 'booking' => 'alnora-clinic-settings-booking', 21 'form' => 'alnora-clinic-settings-form', 21 22 'email' => 'alnora-clinic-settings-email', 22 23 'gdpr' => 'alnora-clinic-settings-gdpr', … … 36 37 'shortcodes' => __( 'Shortcodes', 'alnora-clinic' ), 37 38 'booking' => __( 'Booking rules', 'alnora-clinic' ), 39 'form' => __( 'Form', 'alnora-clinic' ), 38 40 'email' => __( 'Email & reminders', 'alnora-clinic' ), 39 41 'gdpr' => __( 'GDPR', 'alnora-clinic' ), … … 167 169 </div> 168 170 171 <?php /* ── FORM ─────────────────────────────────────────────────── */ ?> 172 <?php elseif ( 'form' === $alnc_tab ) : ?> 173 174 <div class="alnc-card"> 175 <h2 class="alnc-h2"><?php esc_html_e( 'Core fields', 'alnora-clinic' ); ?></h2> 176 <p style="color:var(--alnc-text-muted);margin-bottom:var(--alnc-space-5)"><?php esc_html_e( 'Customize labels, visibility and required status for each field on the booking form.', 'alnora-clinic' ); ?></p> 177 178 <?php 179 /* The same rows and setting keys as Pro's Form tab, so everything set here carries 180 * over on upgrade. Name and email are always on and always required: a booking 181 * without them cannot become a patient record. */ 182 $alnc_core_fields = array( 183 array( 'key' => 'name', 'label_key' => 'form_field_name_label', 'default' => 'Full name', 'always' => true ), 184 array( 'key' => 'email', 'label_key' => 'form_field_email_label', 'default' => 'Email', 'always' => true ), 185 array( 'key' => 'phone', 'label_key' => 'form_field_phone_label', 'default' => 'Phone', 'always' => false ), 186 array( 'key' => 'reason', 'label_key' => 'form_field_reason_label', 'default' => 'Reason for visit (optional)', 'always' => false ), 187 ); 188 ?> 189 190 <div class="alnc-form-fields-table"> 191 <div class="alnc-form-fields-table__head"> 192 <span><?php esc_html_e( 'Field', 'alnora-clinic' ); ?></span> 193 <span><?php esc_html_e( 'Label', 'alnora-clinic' ); ?></span> 194 <span><?php esc_html_e( 'Placeholder', 'alnora-clinic' ); ?></span> 195 <span><?php esc_html_e( 'Required', 'alnora-clinic' ); ?></span> 196 <span><?php esc_html_e( 'Enabled', 'alnora-clinic' ); ?></span> 197 </div> 198 199 <?php foreach ( $alnc_core_fields as $alnc_cf ) : ?> 200 <?php 201 $alnc_ph_key = 'form_field_' . $alnc_cf['key'] . '_placeholder'; 202 $alnc_req_key = 'form_field_' . $alnc_cf['key'] . '_required'; 203 $alnc_en_key = 'form_field_' . $alnc_cf['key'] . '_enabled'; 204 ?> 205 <div class="alnc-form-fields-table__row"> 206 <span class="alnc-badge alnc-badge-navy"><?php echo esc_html( $alnc_cf['key'] ); ?></span> 207 <div class="alnc-field" style="margin:0"> 208 <input class="alnc-input" type="text" 209 name="<?php echo esc_attr( $alnc_cf['label_key'] ); ?>" 210 value="<?php echo esc_attr( (string) ( $settings[ $alnc_cf['label_key'] ] ?? $alnc_cf['default'] ) ); ?>" 211 placeholder="<?php echo esc_attr( $alnc_cf['default'] ); ?>" 212 aria-label="<?php echo esc_attr( sprintf( /* translators: %s: field key, e.g. phone. */ __( 'Label for %s', 'alnora-clinic' ), $alnc_cf['key'] ) ); ?>"> 213 </div> 214 <div class="alnc-field" style="margin:0"> 215 <input class="alnc-input" type="text" 216 name="<?php echo esc_attr( $alnc_ph_key ); ?>" 217 value="<?php echo esc_attr( (string) ( $settings[ $alnc_ph_key ] ?? '' ) ); ?>" 218 placeholder="<?php esc_attr_e( 'Shown inside the empty field', 'alnora-clinic' ); ?>" 219 aria-label="<?php echo esc_attr( sprintf( /* translators: %s: field key, e.g. phone. */ __( 'Placeholder for %s', 'alnora-clinic' ), $alnc_cf['key'] ) ); ?>"> 220 </div> 221 <div class="alnc-form-fields-table__toggle"> 222 <?php if ( $alnc_cf['always'] ) : ?> 223 <span class="alnc-badge alnc-badge-teal"><?php esc_html_e( 'Always', 'alnora-clinic' ); ?></span> 224 <?php else : ?> 225 <input type="hidden" name="<?php echo esc_attr( $alnc_req_key ); ?>" value="0"> 226 <label class="alnc-switch"> 227 <input type="checkbox" name="<?php echo esc_attr( $alnc_req_key ); ?>" value="1" <?php checked( ! empty( $settings[ $alnc_req_key ] ) ); ?>> 228 <span></span> 229 </label> 230 <?php endif; ?> 231 </div> 232 <div class="alnc-form-fields-table__toggle"> 233 <?php if ( $alnc_cf['always'] ) : ?> 234 <span class="alnc-badge alnc-badge-teal"><?php esc_html_e( 'Always', 'alnora-clinic' ); ?></span> 235 <?php else : ?> 236 <input type="hidden" name="<?php echo esc_attr( $alnc_en_key ); ?>" value="0"> 237 <label class="alnc-switch"> 238 <input type="checkbox" name="<?php echo esc_attr( $alnc_en_key ); ?>" value="1" <?php checked( ! empty( $settings[ $alnc_en_key ] ) ); ?>> 239 <span></span> 240 </label> 241 <?php endif; ?> 242 </div> 243 </div> 244 <?php endforeach; ?> 245 </div> 246 247 <p class="alnc-field-hint" style="margin-top:var(--alnc-space-4)"><?php esc_html_e( 'In the Pro version you can add your own questions, show a field only for certain doctors or services, split the name into name and surname,', 'alnora-clinic' ); ?> <a href="https://alnora-booking.com/docs/#booking-form-fields" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'and much more', 'alnora-clinic' ); ?></a>.</p> 248 </div> 249 169 250 <?php /* ── EMAIL & REMINDERS ──────────────────────────────────── */ ?> 170 251 <?php elseif ( 'email' === $alnc_tab ) : ?> -
alnora-clinic/trunk/alnora-clinic.php
r3742445 r3744939 6 6 * Plugin URI: https://alnora-booking.com 7 7 * Description: Appointment booking for private medical clinics — doctors, services, schedules, patient records and automated email notifications. 8 * Version: 1.3. 68 * Version: 1.3.7 9 9 * Requires at least: 6.0 10 10 * Requires PHP: 7.4 … … 62 62 |-------------------------------------------------------------------------- 63 63 */ 64 if ( ! defined( 'ALNC_VERSION' ) ) { define( 'ALNC_VERSION', '1.3. 6' ); }64 if ( ! defined( 'ALNC_VERSION' ) ) { define( 'ALNC_VERSION', '1.3.7' ); } 65 65 if ( ! defined( 'ALNC_DB_VERSION' ) ) { define( 'ALNC_DB_VERSION', '1.7.2' ); } 66 66 if ( ! defined( 'ALNC_FILE' ) ) { define( 'ALNC_FILE', __FILE__ ); } -
alnora-clinic/trunk/api/class-rest-routes.php
r3604289 r3744939 359 359 } 360 360 361 $result = Appointment_Manager::create_booking( (array) $params ); 361 // Settings → Form: a field switched off is never stored, and a required one must 362 // be filled. Checked here as well as in the form, so the rule holds for any client. 363 $params = (array) $params; 364 foreach ( array( 'phone' => 'patient_phone', 'reason' => 'reason' ) as $field => $param ) { 365 $conf = \AlnoraClinic\Frontend\Frontend::core_fields()[ $field ]; 366 if ( ! $conf['enabled'] ) { 367 unset( $params[ $param ] ); 368 } elseif ( $conf['required'] && '' === trim( (string) ( $params[ $param ] ?? '' ) ) ) { 369 // In the patient language, like the rest of the form. 370 $strings = \AlnoraClinic\Translations::get( (string) \AlnoraClinic\Settings::get( 'language', 'en_US' ) ); 371 return new \WP_Error( 'alnc_missing_field', (string) ( $strings['required'] ?? __( 'Please fill in all required fields.', 'alnora-clinic' ) ), array( 'status' => 400 ) ); 372 } 373 } 374 375 $result = Appointment_Manager::create_booking( $params ); 362 376 363 377 if ( is_wp_error( $result ) ) { -
alnora-clinic/trunk/frontend/class-frontend.php
r3687687 r3744939 105 105 106 106 /** 107 * The core booking fields as set under Settings → Form. 108 * 109 * A label still at its English default is sent empty, so the form shows its own 110 * translation in the patient language; only a label the clinic changed is sent as 111 * typed. Name and email are always shown and always required. 112 * 113 * @return array<string,array<string,mixed>> 114 */ 115 public static function core_fields(): array { 116 $defaults = array( 117 'name' => 'Full name', 118 'email' => 'Email', 119 'phone' => 'Phone', 120 'reason' => 'Reason for visit (optional)', 121 ); 122 $fields = array(); 123 foreach ( $defaults as $key => $default ) { 124 $label = trim( (string) Settings::get( 'form_field_' . $key . '_label', $default ) ); 125 $fixed = in_array( $key, array( 'name', 'email' ), true ); 126 127 $fields[ $key ] = array( 128 'label' => $label === $default ? '' : $label, 129 'placeholder' => (string) Settings::get( 'form_field_' . $key . '_placeholder', '' ), 130 'enabled' => $fixed || (bool) Settings::get( 'form_field_' . $key . '_enabled', 1 ), 131 'required' => $fixed || (bool) Settings::get( 'form_field_' . $key . '_required', 0 ), 132 ); 133 } 134 return $fields; 135 } 136 137 /** 107 138 * Shortcode handler. 108 139 * … … 156 187 'timeFormat' => (string) get_option( 'time_format', 'g:i a' ), 157 188 'i18n' => \AlnoraClinic\Translations::get( $lang_code ), 189 'fields' => self::core_fields(), 158 190 ); 159 191 -
alnora-clinic/trunk/frontend/js/alnc-booking.js
r3687687 r3744939 483 483 ] ); 484 484 485 // Core fields as set under Settings → Form. An empty label means "still the 486 // default", so the patient-language translation is shown instead. 487 var F = CFG.fields || {}; 488 function conf( key ) { 489 var c = F[ key ] || {}; 490 var fixed = ( 'name' === key || 'email' === key ); 491 var label = c.label || T[ key ] || key; 492 // The default reason label ends in "(optional)" in every language; drop it 493 // once the clinic makes the field required. 494 if ( ! c.label && c.required ) { label = label.replace( /\s*\([^)]*\)\s*$/, '' ); } 495 return { 496 label: label + ( ( fixed || c.required ) ? ' *' : '' ), 497 placeholder: c.placeholder || '', 498 enabled: fixed || c.enabled !== false, 499 required: fixed || !! c.required 500 }; 501 } 502 var cName = conf( 'name' ), cEmail = conf( 'email' ), cPhone = conf( 'phone' ), cReason = conf( 'reason' ); 503 485 504 var form = el( 'form', { class: 'alnc-form', autocomplete: 'on' } ); 486 form.appendChild( field( 'alnc-f-name', T.name + ' *', 'text', 'name', true ) ); 487 form.appendChild( el( 'div', { class: 'alnc-form__row' }, [ 488 field( 'alnc-f-email', T.email + ' *', 'email', 'email', true ), 489 field( 'alnc-f-phone', T.phone, 'tel', 'phone', false ) 490 ] ) ); 491 form.appendChild( textareaField( 'alnc-f-reason', T.reason, 'reason' ) ); 505 form.appendChild( field( 'alnc-f-name', cName.label, 'text', 'name', true, cName.placeholder ) ); 506 var emailField = field( 'alnc-f-email', cEmail.label, 'email', 'email', true, cEmail.placeholder ); 507 if ( cPhone.enabled ) { 508 // Email and phone share a two-column row. 509 form.appendChild( el( 'div', { class: 'alnc-form__row' }, [ 510 emailField, 511 field( 'alnc-f-phone', cPhone.label, 'tel', 'tel', cPhone.required, cPhone.placeholder ) 512 ] ) ); 513 } else { 514 // No phone: email takes the full width, like the name above it. 515 form.appendChild( emailField ); 516 } 517 if ( cReason.enabled ) { 518 form.appendChild( textareaField( 'alnc-f-reason', cReason.label, 'reason', cReason.required, cReason.placeholder ) ); 519 } 492 520 493 521 // Recurring (Pro): optional repeat controls. … … 527 555 var name = form.querySelector( '#alnc-f-name' ).value.trim(); 528 556 var email = form.querySelector( '#alnc-f-email' ).value.trim(); 529 var phone = form.querySelector( '#alnc-f-phone' ).value.trim(); 530 var reason = form.querySelector( '#alnc-f-reason' ).value.trim(); 557 var phoneEl = form.querySelector( '#alnc-f-phone' ); 558 var reasonEl = form.querySelector( '#alnc-f-reason' ); 559 var phone = phoneEl ? phoneEl.value.trim() : ''; 560 var reason = reasonEl ? reasonEl.value.trim() : ''; 531 561 var consented = form.querySelector( '#' + consentId ).checked; 532 562 533 563 if ( ! name || ! email ) { return showErr( T.required ); } 564 if ( ( phoneEl && cPhone.required && ! phone ) || ( reasonEl && cReason.required && ! reason ) ) { return showErr( T.required ); } 534 565 if ( ! consented ) { return showErr( T.consentReq ); } 535 566 … … 696 727 697 728 /* ----- field factories ----- */ 698 function field( id, label, type, name, required ) {729 function field( id, label, type, name, required, placeholder ) { 699 730 return el( 'div', { class: 'alnc-field' }, [ 700 731 el( 'label', { class: 'alnc-field-label', for: id, text: label } ), 701 el( 'input', { class: 'alnc-input', id: id, name: name, type: type, required: required ? 'required' : null, autocomplete: name } )732 el( 'input', { class: 'alnc-input', id: id, name: name, type: type, required: required ? 'required' : null, placeholder: placeholder || null, autocomplete: name } ) 702 733 ] ); 703 734 } 704 function textareaField( id, label, name ) {735 function textareaField( id, label, name, required, placeholder ) { 705 736 return el( 'div', { class: 'alnc-field' }, [ 706 737 el( 'label', { class: 'alnc-field-label', for: id, text: label } ), 707 el( 'textarea', { class: 'alnc-textarea', id: id, name: name, rows: '3' } )738 el( 'textarea', { class: 'alnc-textarea', id: id, name: name, rows: '3', required: required ? 'required' : null, placeholder: placeholder || null } ) 708 739 ] ); 709 740 } -
alnora-clinic/trunk/includes/class-appointment-manager.php
r3585842 r3744939 385 385 386 386 /** 387 * Generate a unique reference number, e.g. alnc-2026-0042. 387 * Generate a unique reference number, e.g. 26-0042. 388 * 389 * Two-digit year, the same as Pro, so a clinic moving between the two keeps one 390 * numbering style. References issued earlier as 2026-0042 stay as they are: they 391 * cannot collide with the new 26- series, which is counted on its own. 388 392 * 389 393 * @return string … … 392 396 global $wpdb; 393 397 $table = self::table(); 394 $year = current_time( 'Y' );398 $year = (string) current_time( 'y' ); 395 399 396 400 // Next sequence = highest existing sequence for this year + 1. Using MAX (not … … 399 403 // failing the insert ("Could not save the appointment"). 400 404 $like = $wpdb->esc_like( $year . '-' ) . '%'; 401 $max = (int) $wpdb->get_var( $wpdb->prepare( "SELECT MAX( CAST( SUBSTRING_INDEX( ref_number, '-', -1 ) AS UNSIGNED )) FROM {$table} WHERE ref_number LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared405 $max = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE( MAX( CAST( SUBSTRING_INDEX( ref_number, '-', -1 ) AS UNSIGNED ) ), 0 ) FROM {$table} WHERE ref_number LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 402 406 $seq = $max + 1; 403 407 -
alnora-clinic/trunk/includes/class-settings.php
r3657434 r3744939 44 44 // a reinstall. Turn on only if you want everything erased on uninstall. 45 45 'delete_data_on_uninstall' => 0, 46 // Core booking-form fields (Settings → Form). Same keys and defaults as Pro, 47 // so a clinic's labels and switches carry over unchanged on upgrade. 48 'form_field_name_label' => 'Full name', 49 'form_field_email_label' => 'Email', 50 'form_field_phone_label' => 'Phone', 51 'form_field_phone_enabled' => 1, 52 'form_field_phone_required' => 0, 53 'form_field_reason_label' => 'Reason for visit (optional)', 54 'form_field_reason_enabled' => 1, 55 'form_field_reason_required' => 0, 56 'form_field_name_placeholder' => '', 57 'form_field_email_placeholder' => '', 58 'form_field_phone_placeholder' => '+1 212 555 0147', 59 'form_field_reason_placeholder' => '', 46 60 ); 47 61 } -
alnora-clinic/trunk/readme.txt
r3742445 r3744939 5 5 Tested up to: 7.1.2 6 6 Requires PHP: 7.4 7 Stable tag: 1.3. 67 Stable tag: 1.3.7 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 19 19 No generic salon plugin bent into shape. No spreadsheets. No five add-ons duct-taped together. Just one focused product that understands doctors, services, patients and privacy. 20 20 21 👉 [Live demo](https://alnora-booking.com/demo/) · [See Pro pricing](https://alnora-booking.com/pricing/) · [Documentation](https://alnora-booking.com/docs/) 21 👉 [Live demo](https://alnora-booking.com/demo/) · [See Pro pricing](https://alnora-booking.com/pricing/) · [Documentation](https://alnora-booking.com/docs/) · [YouTube](https://www.youtube.com/@alnora-clinic) 22 22 23 23 **Why private clinics choose Alnora** … … 150 150 == Changelog == 151 151 152 = 1.3.7 = 153 * New: **Settings → Form.** Change the label and placeholder of each field on the booking form: name, email, phone and reason for visit. Phone and reason each have their own **Required** and **Enabled** switches, so you can ask for a phone number on every booking or leave the reason box off the form entirely. Name and email are always shown and always required. A label you leave as it is still shows in the patient's language. When phone is switched off, the email field takes the full width. 154 * New: a required field is marked with an asterisk and checked when the booking is sent, as well as in the form. A field you switch off is never saved, even if something else sends it. 155 * Improvement: **shorter appointment references.** New bookings get references such as 26-0042 instead of 2026-0042, the same style as Pro, so a clinic moving between the two keeps one numbering style. References already issued stay as they are. 156 152 157 = 1.3.6 = 153 158 * Updated Freemius SDK to the latest version. … … 257 262 == Upgrade Notice == 258 263 264 = 1.3.7 = 265 New Settings → Form tab to relabel the booking form's fields and make phone or reason required or hidden. New appointment references use the shorter 26-0042 style. Existing references are unchanged. No database changes. 266 259 267 = 1.3.6 = 260 268 Updated Freemius SDK to the latest version. No database or configuration changes.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)