admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpim_lend_ajax' ), 'i18n' => [ 'remove' => __( 'Remove', 'borrowdesk' ), 'available' => __( 'available', 'borrowdesk' ), 'chooseUnit' => __( 'Choose a unit', 'borrowdesk' ), 'noResults' => __( 'No matches', 'borrowdesk' ), 'notLendable' => __( 'not set up for lending', 'borrowdesk' ), ], ] ); } /* ================================================================== helpers */ private static function guard( $nonce_action ) { if ( ! WPIMLend::can_manage() ) { wp_die( esc_html__( 'You are not allowed to do that.', 'borrowdesk' ), 403 ); } check_admin_referer( $nonce_action ); } private static function redirect( $page, array $args = [] ) { wp_safe_redirect( add_query_arg( array_merge( [ 'page' => $page ], $args ), admin_url( 'admin.php' ) ) ); exit; } private static function flash( $message, $type = 'success' ) { set_transient( 'wpim_lend_flash_' . get_current_user_id(), [ 'message' => $message, 'type' => $type ], 60 ); } private static function render_flash() { $key = 'wpim_lend_flash_' . get_current_user_id(); $flash = get_transient( $key ); if ( $flash ) { delete_transient( $key ); printf( '

%s

', esc_attr( 'error' === $flash['type'] ? 'error' : 'success' ), esc_html( $flash['message'] ) ); } } private static function post( $key, $default = '' ) { return isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : $default; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce checked in guard(); each caller sanitizes } private static function get( $key, $default = '' ) { return isset( $_GET[ $key ] ) ? sanitize_text_field( wp_unslash( $_GET[ $key ] ) ) : $default; // phpcs:ignore WordPress.Security.NonceVerification } public static function format_date( $ymd ) { return $ymd ? date_i18n( get_option( 'date_format' ), strtotime( $ymd . ' 12:00:00' ) ) : ''; } public static function format_utc( $datetime ) { return $datetime ? get_date_from_gmt( $datetime, get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) : ''; } private static function item_label( $core ) { if ( ! $core ) { return __( '(deleted item)', 'borrowdesk' ); } return $core->inventory_number ? $core->inventory_number . ' — ' . $core->inventory_name : $core->inventory_name; } /* ================================================================== dashboard */ public static function page_dashboard() { if ( ! WPIMLend::can_manage() ) { return; } $loan_id = absint( self::get( 'loan' ) ); if ( $loan_id ) { self::page_loan( $loan_id ); return; } $views = [ 'open' => __( 'On loan', 'borrowdesk' ), 'due_today' => __( 'Due today', 'borrowdesk' ), 'overdue' => __( 'Overdue', 'borrowdesk' ), 'returned' => __( 'Returned (30 days)', 'borrowdesk' ), ]; $view = self::get( 'view', 'open' ); $view = isset( $views[ $view ] ) ? $view : 'open'; $today = WPIMLend_Availability::today(); $filters = [ 'borrower_id' => absint( self::get( 'borrower_id' ) ), 'inventory_id' => absint( self::get( 'inventory_id' ) ), 'group' => self::get( 'group' ), ]; $paged = max( 1, absint( self::get( 'paged', 1 ) ) ); $total = WPIMLend_Repo::list_loans( $view, $today, $filters, 0, 0, TRUE ); $loans = WPIMLend_Repo::list_loans( $view, $today, $filters, self::PER_PAGE, ( $paged - 1 ) * self::PER_PAGE ); $lines = WPIMLend_Repo::lines_for_loans( wp_list_pluck( $loans, 'loan_id' ) ); $item_ids = []; foreach ( $lines as $ls ) { foreach ( $ls as $l ) { $item_ids[] = (int) $l->inventory_id; } } $core = WPIMLend_Repo::get_core_items( $item_ids ); $base = admin_url( 'admin.php?page=' . self::PAGE_DASHBOARD ); ?>


status && $loan->due_date < $today; $view_url = add_query_arg( 'loan', $loan->loan_id, $base ); ?>
borrower_name ); ?> borrower_group ) : ?>
borrower_group ); ?>
loan_id ] ) ? $lines[ $loan->loan_id ] : [] as $line ) : $out = (int) $line->quantity - (int) $line->returned_quantity; $unit = $line->unit_id ? WPIMLend_Repo::get_unit( $line->unit_id ) : NULL; ?>
inventory_id ] ) ? $core[ $line->inventory_id ] : NULL ) ); ?> code ); ?> × quantity; ?> status && $out < (int) $line->quantity && $out > 0 ) : ?> ()
out_at ) ); ?> due_date ) ); ?>
status ? esc_html__( 'Return / extend', 'borrowdesk' ) : esc_html__( 'View', 'borrowdesk' ); ?> status && $loan->borrower_email ) : ?>
'; echo wp_kses_post( paginate_links( [ 'base' => add_query_arg( 'paged', '%#%', $url ), 'format' => '', 'current' => $paged, 'total' => $pages, ] ) ); echo '
'; } /* ================================================================== single loan: return / extend */ private static function page_loan( $loan_id ) { $loan = WPIMLend_Repo::get_loan( $loan_id ); if ( ! $loan ) { echo '

' . esc_html__( 'Loan not found.', 'borrowdesk' ) . '

'; return; } $borrower = WPIMLend_Repo::get_borrower( $loan->borrower_id ); $lines = WPIMLend_Repo::get_lines( $loan_id ); $core = WPIMLend_Repo::get_core_items( wp_list_pluck( $lines, 'inventory_id' ) ); $returns = WPIMLend_Repo::get_return_events( wp_list_pluck( $lines, 'line_id' ) ); $open = 'open' === $loan->status; ?>

loan_id ); ?>

←

notes ) : ?>
name : '' ); ?>email ? ' <' . esc_html( $borrower->email ) . '>' : ''; ?>
out_at ) ); ?>
due_date ) ); ?>
loan_id ); ?>
notes ) ); ?>
closed_at ) ) ); ?>

loan_id ); ?> quantity - (int) $line->returned_quantity; $unit = $line->unit_id ? WPIMLend_Repo::get_unit( $line->unit_id ) : NULL; ?>
inventory_id ] ) ? $core[ $line->inventory_id ] : NULL ) ); ?> code ); ?> quantity; ?> 0 ) : ?> 0 ) : ?> 0 ) : ?>

$row ) { $qty = isset( $row['quantity'] ) ? (int) $row['quantity'] : 0; if ( $qty < 1 ) { continue; } $line = WPIMLend_Repo::get_line( absint( $line_id ) ); if ( ! $line || (int) $line->loan_id !== $loan_id ) { continue; } $any = TRUE; $r = WPIMLend_Service::return_line( $line->line_id, $qty, sanitize_key( $row['condition'] ?? 'ok' ), sanitize_text_field( $row['note'] ?? '' ) ); if ( is_wp_error( $r ) ) { $result = $r; } } if ( ! $any ) { $result = new WP_Error( 'none', __( 'Enter a quantity to return, or use "Return everything".', 'borrowdesk' ) ); } } if ( is_wp_error( $result ) ) { self::flash( $result->get_error_message(), 'error' ); } else { self::flash( __( 'Return recorded.', 'borrowdesk' ) ); } self::redirect( self::PAGE_DASHBOARD, [ 'loan' => $loan_id ] ); } public static function handle_extend() { $loan_id = absint( self::post( 'loan_id' ) ); self::guard( 'wpim_lend_extend_' . $loan_id ); $result = WPIMLend_Service::extend( $loan_id, sanitize_text_field( self::post( 'due_date' ) ) ); self::flash( is_wp_error( $result ) ? $result->get_error_message() : __( 'Due date changed.', 'borrowdesk' ), is_wp_error( $result ) ? 'error' : 'success' ); self::redirect( self::PAGE_DASHBOARD, [ 'loan' => $loan_id ] ); } public static function handle_remind() { $loan_id = absint( self::get( 'loan' ) ); self::guard( 'wpim_lend_remind_' . $loan_id ); $sent = WPIMLend_Mail::send_reminder_for_loan( $loan_id ); self::flash( $sent ? __( 'Reminder sent.', 'borrowdesk' ) : __( 'The reminder could not be sent (no email address?).', 'borrowdesk' ), $sent ? 'success' : 'error' ); self::redirect( self::PAGE_DASHBOARD ); } /* ================================================================== check out */ public static function page_checkout() { if ( ! WPIMLend::can_manage() ) { return; } $preset_borrower = absint( self::get( 'borrower' ) ); $borrower = $preset_borrower ? WPIMLend_Repo::get_borrower( $preset_borrower ) : NULL; ?>

      absint( $row['inventory_id'] ?? 0 ), 'quantity' => (int) ( $row['quantity'] ?? 1 ), 'unit_id' => absint( $row['unit_id'] ?? 0 ), ]; } $result = WPIMLend_Service::checkout( $borrower_id, $lines, sanitize_text_field( self::post( 'due_date' ) ), sanitize_textarea_field( self::post( 'notes' ) ) ); if ( is_wp_error( $result ) ) { self::flash( $result->get_error_message(), 'error' ); self::redirect( self::PAGE_CHECKOUT, $borrower_id ? [ 'borrower' => $borrower_id ] : [] ); } self::flash( __( 'Checked out.', 'borrowdesk' ) ); self::redirect( self::PAGE_DASHBOARD, [ 'loan' => $result ] ); } /* ================================================================== AJAX */ private static function ajax_guard() { if ( ! WPIMLend::can_manage() || ! check_ajax_referer( 'wpim_lend_ajax', 'nonce', FALSE ) ) { wp_send_json_error( [], 403 ); } } public static function ajax_search_borrowers() { self::ajax_guard(); $term = sanitize_text_field( wp_unslash( $_GET['term'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification $out = []; foreach ( WPIMLend_Repo::search_borrowers( $term, 15 ) as $b ) { $out[] = [ 'id' => (int) $b->borrower_id, 'label' => $b->name . ( $b->email ? ' <' . $b->email . '>' : '' ) . ( $b->borrower_group ? ' — ' . $b->borrower_group : '' ), 'name' => $b->name, ]; } wp_send_json_success( $out ); } public static function ajax_search_items() { self::ajax_guard(); $term = sanitize_text_field( wp_unslash( $_GET['term'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification $items = WPIMLend_Repo::search_core_items( $term, 20 ); $avail = WPIMLend_Availability::for_items( wp_list_pluck( $items, 'inventory_id' ) ); $out = []; foreach ( $items as $item ) { $a = isset( $avail[ $item->inventory_id ] ) ? $avail[ $item->inventory_id ] : NULL; if ( ! $a ) { continue; } $out[] = [ 'id' => (int) $item->inventory_id, 'label' => self::item_label( $item ), 'available' => max( 0, (int) $a['available'] ), 'owned' => (int) $a['owned'], 'lendable' => (bool) $a['lendable'], 'trackUnits' => (bool) $a['track_units'], 'dueDate' => WPIMLend_Availability::default_due_date( $item->inventory_id ), ]; } wp_send_json_success( $out ); } public static function ajax_item_units() { self::ajax_guard(); $inventory_id = absint( $_GET['inventory_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification $on_loan = WPIMLend_Repo::units_on_loan( $inventory_id ); $out = []; foreach ( WPIMLend_Repo::get_units( $inventory_id ) as $unit ) { if ( 'available' === $unit->status && ! isset( $on_loan[ (int) $unit->unit_id ] ) ) { $out[] = [ 'id' => (int) $unit->unit_id, 'label' => $unit->code . ( $unit->serial ? ' (' . $unit->serial . ')' : '' ), ]; } } wp_send_json_success( $out ); } /* ================================================================== borrowers */ private static function clean_borrower( array $in ) { return [ 'name' => sanitize_text_field( $in['name'] ?? '' ), 'email' => sanitize_email( $in['email'] ?? '' ), 'phone' => sanitize_text_field( $in['phone'] ?? '' ), 'borrower_group' => sanitize_text_field( $in['borrower_group'] ?? '' ), 'notes' => sanitize_textarea_field( $in['notes'] ?? '' ), 'user_id' => absint( $in['user_id'] ?? 0 ), ]; } public static function page_borrowers() { if ( ! WPIMLend::can_manage() ) { return; } $borrower_id = absint( self::get( 'borrower' ) ); $editing = 'new' === self::get( 'borrower' ) || $borrower_id; ?>


      name ); ?> email ); ?> phone ); ?> borrower_group ); ?>

      'b[user_id]', 'id' => 'b-user', 'show_option_none' => __( '— None —', 'borrowdesk' ), 'option_none_value' => 0, 'selected' => $b ? (int) $b->user_id : 0 ] ); ?>

      $b->borrower_id ], 100 ); $lines = WPIMLend_Repo::lines_for_loans( wp_list_pluck( $loans, 'loan_id' ) ); $ids = []; foreach ( $lines as $ls ) { foreach ( $ls as $l ) { $ids[] = $l->inventory_id; } } $core = WPIMLend_Repo::get_core_items( $ids ); ?>

      loan_id ] ) ? $lines[ $loan->loan_id ] : [] as $l ) : ?>
      inventory_id ] ) ? $core[ $l->inventory_id ] : NULL ) ); ?> × quantity; ?>
      out_at ) ); ?> due_date ) ); ?> status ? esc_html__( 'Open', 'borrowdesk' ) : esc_html__( 'Returned', 'borrowdesk' ); ?>
      'new' ] ); } $id = WPIMLend_Repo::save_borrower( $data, absint( self::post( 'borrower_id' ) ) ); self::flash( $id ? __( 'Borrower saved.', 'borrowdesk' ) : __( 'The borrower could not be saved.', 'borrowdesk' ), $id ? 'success' : 'error' ); self::redirect( self::PAGE_BORROWERS, $id ? [ 'borrower' => $id ] : [] ); } public static function handle_borrower_import() { self::guard( 'wpim_lend_borrower_import' ); $result = WPIMLend_CSV::import_borrowers( $_FILES['csv'] ?? NULL ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing -- nonce checked in guard(); the file is validated in import_borrowers() self::flash( $result['message'], $result['ok'] ? 'success' : 'error' ); self::redirect( self::PAGE_BORROWERS ); } public static function handle_export() { if ( ! WPIMLend::can_manage() ) { wp_die( esc_html__( 'You are not allowed to do that.', 'borrowdesk' ), 403 ); } check_admin_referer( 'wpim_lend_export' ); WPIMLend_CSV::export_loans( sanitize_key( self::get( 'view', 'open' ) ) ); exit; } /* ================================================================== settings */ public static function page_settings() { if ( ! current_user_can( 'manage_options' ) ) { return; } $s = WPIMLend_Settings::all(); global $wpdb; $categories = $wpdb->get_results( 'SELECT category_id, category_name FROM ' . $wpdb->prefix . 'wpinventory_category ORDER BY category_name' ); // phpcs:ignore ?>

      ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>