{"id":359489,"date":"2026-08-27T13:29:48","date_gmt":"2026-08-27T13:29:48","guid":{"rendered":"https:\/\/fr.wordpress.org\/plugins\/sprint-collect\/"},"modified":"2026-09-02T11:33:00","modified_gmt":"2026-09-02T11:33:00","slug":"sprint-collect","status":"publish","type":"plugin","link":"https:\/\/bal.wordpress.org\/plugins\/sprint-collect\/","author":23539978,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.1","stable_tag":"1.0.1","tested":"7.1","requires":"6.5","requires_php":"8.0","requires_plugins":null,"header_name":"Sprint Collect","header_author":"Sprint Plugins","header_description":"Securely collect, review, and manage supporting documents for WordPress users.","assets_banners_color":"ebecfc","last_updated":"2026-09-02 11:33:00","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/sprintplugins.fr\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":82,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"sprintplugins","date":"2026-08-27 13:29:21","revision":3668927},"1.0.1":{"tag":"1.0.1","author":"sprintplugins","date":"2026-09-02 11:33:00","revision":3677875}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3668959,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3668959,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3668959,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500-fr_FR.png":{"filename":"banner-1544x500-fr_FR.png","revision":3668959,"resolution":"1544x500","location":"assets","locale":"fr_FR","width":1544,"height":500},"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3668959,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250-fr_FR.png":{"filename":"banner-772x250-fr_FR.png","revision":3668959,"resolution":"772x250","location":"assets","locale":"fr_FR","width":772,"height":250},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3668959,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3677876,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=sprint-collect&tab=dashboard\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"latest\"},\"features\":{\"networking\":true},\"steps\":[{\"step\":\"setSiteOptions\",\"options\":{\"blogname\":\"Sprint Collect Demo\",\"blogdescription\":\"Fictitious sports-club supporting-document workflow\",\"permalink_structure\":\"\\\/%postname%\\\/\"}},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"sprint-collect\"},\"options\":{\"activate\":true}},{\"step\":\"runPHP\",\"code\":\"<?php\\nrequire_once '\\\/wordpress\\\/wp-load.php';\\n\\nif ( ! class_exists( 'Sprint_Collect_Repository' ) || ! class_exists( 'Sprint_Collect_Storage' ) ) {\\n\\treturn;\\n}\\n\\nif ( get_option( 'sprint_collect_demo_seed_v1' ) ) {\\n\\treturn;\\n}\\n\\nglobal $wpdb;\\n\\n$repository = new Sprint_Collect_Repository();\\n$now        = current_time( 'mysql' );\\n$today_ts   = current_time( 'timestamp' );\\n$today      = date( 'Y-m-d', $today_ts );\\n$minus_3    = date( 'Y-m-d', strtotime( '-3 days', $today_ts ) );\\n$minus_7    = date( 'Y-m-d', strtotime( '-7 days', $today_ts ) );\\n$minus_14   = date( 'Y-m-d', strtotime( '-14 days', $today_ts ) );\\n$minus_30   = date( 'Y-m-d', strtotime( '-30 days', $today_ts ) );\\n$plus_12    = date( 'Y-m-d', strtotime( '+12 days', $today_ts ) );\\n$plus_60    = date( 'Y-m-d', strtotime( '+60 days', $today_ts ) );\\n$plus_180   = date( 'Y-m-d', strtotime( '+180 days', $today_ts ) );\\n$plus_365   = date( 'Y-m-d', strtotime( '+365 days', $today_ts ) );\\n\\nupdate_option( 'blogname', 'Sprint Collect Demo', false );\\nupdate_option( 'blogdescription', 'Fictitious sports-club supporting-document workflow', false );\\nupdate_option( 'permalink_structure', '\\\/%postname%\\\/', false );\\n\\n$admin = get_user_by( 'login', 'admin' );\\n$admin_id = $admin ? (int) $admin->ID : 1;\\nif ( $admin_id ) {\\n\\twp_update_user(\\n\\t\\tarray(\\n\\t\\t\\t'ID'           => $admin_id,\\n\\t\\t\\t'display_name' => 'Demo Administrator',\\n\\t\\t\\t'nickname'     => 'Demo Administrator',\\n\\t\\t\\t'user_email'   => 'admin@example.com',\\n\\t\\t)\\n\\t);\\n}\\n\\n$create_user = static function( $login, $display_name, $first_name, $last_name ) {\\n\\t$email = $login . '@example.com';\\n\\t$user = get_user_by( 'login', $login );\\n\\tif ( ! $user ) {\\n\\t\\t$user_id = wp_create_user( $login, 'demo-password', $email );\\n\\t\\tif ( is_wp_error( $user_id ) ) {\\n\\t\\t\\treturn 0;\\n\\t\\t}\\n\\t\\t$user = get_user_by( 'id', $user_id );\\n\\t}\\n\\tif ( ! $user ) {\\n\\t\\treturn 0;\\n\\t}\\n\\twp_update_user(\\n\\t\\tarray(\\n\\t\\t\\t'ID'           => $user->ID,\\n\\t\\t\\t'display_name' => $display_name,\\n\\t\\t\\t'nickname'     => $display_name,\\n\\t\\t\\t'first_name'   => $first_name,\\n\\t\\t\\t'last_name'    => $last_name,\\n\\t\\t\\t'user_email'   => $email,\\n\\t\\t\\t'role'         => 'subscriber',\\n\\t\\t)\\n\\t);\\n\\treturn (int) $user->ID;\\n};\\n\\n$emma_id   = $create_user( 'emma.carter', 'Emma Carter', 'Emma', 'Carter' );\\n$liam_id   = $create_user( 'liam.wilson', 'Liam Wilson', 'Liam', 'Wilson' );\\n$sofia_id  = $create_user( 'sofia.martin', 'Sofia Martin', 'Sofia', 'Martin' );\\n$noah_id   = $create_user( 'noah.brown', 'Noah Brown', 'Noah', 'Brown' );\\n$maya_id   = $create_user( 'maya.lee', 'Maya Lee', 'Maya', 'Lee' );\\n$oliver_id = $create_user( 'oliver.king', 'Oliver King', 'Oliver', 'King' );\\n\\n$settings = get_option( 'sprint_collect_settings', array() );\\n$settings = is_array( $settings ) ? $settings : array();\\n$settings['display_mode']            = 'complete';\\n$settings['notify_on_upload']        = 0;\\n$settings['notify_on_status_change'] = 0;\\n$settings['admin_email']             = 'admin@example.com';\\n$settings['email_from_name']         = 'Sprint Collect Demo';\\n$settings['email_from_address']      = 'admin@example.com';\\n$settings['notification_user_ids']   = array();\\n$settings['notification_roles']      = array();\\n$settings['reminders_enabled']       = 0;\\n$settings['expiration_notice_days']  = 30;\\nupdate_option( 'sprint_collect_settings', $settings, false );\\n\\nif ( class_exists( 'Sprint_Collect_Display_Mode' ) ) {\\n\\tSprint_Collect_Display_Mode::update_site_default( Sprint_Collect_Display_Mode::COMPLETE );\\n\\tif ( $admin_id ) {\\n\\t\\tSprint_Collect_Display_Mode::update_user_preference( Sprint_Collect_Display_Mode::COMPLETE, $admin_id );\\n\\t}\\n}\\n\\n$types_by_slug = array();\\nforeach ( $repository->get_document_types() as $type ) {\\n\\t$types_by_slug[ $type->slug ] = $type;\\n}\\n\\n$configure_type = static function( $slug, $data ) use ( $repository, &$types_by_slug ) {\\n\\tif ( empty( $types_by_slug[ $slug ] ) ) {\\n\\t\\treturn 0;\\n\\t}\\n\\t$type = $types_by_slug[ $slug ];\\n\\t$id = $repository->save_document_type(\\n\\t\\tarray(\\n\\t\\t\\t'name'               => $data['name'],\\n\\t\\t\\t'slug'               => $slug,\\n\\t\\t\\t'description'        => $data['description'],\\n\\t\\t\\t'allowed_extensions' => 'pdf,jpg,jpeg,png',\\n\\t\\t\\t'max_size_mb'        => 5,\\n\\t\\t\\t'has_expiration'     => ! empty( $data['has_expiration'] ) ? 1 : 0,\\n\\t\\t\\t'has_document_date'  => ! empty( $data['has_document_date'] ) ? 1 : 0,\\n\\t\\t\\t'retention_mode'     => 'manual',\\n\\t\\t\\t'retention_days'     => 0,\\n\\t\\t\\t'source_type'        => 'upload',\\n\\t\\t\\t'source_settings'    => array(),\\n\\t\\t),\\n\\t\\t(int) $type->id\\n\\t);\\n\\tif ( $id ) {\\n\\t\\t$types_by_slug[ $slug ] = $repository->get_document_type( $id );\\n\\t}\\n\\treturn $id;\\n};\\n\\n$configure_type(\\n\\t'identity-document',\\n\\tarray(\\n\\t\\t'name' => 'Identity document',\\n\\t\\t'description' => 'Example identity document used to illustrate a member registration workflow.',\\n\\t\\t'has_expiration' => 1,\\n\\t\\t'has_document_date' => 1,\\n\\t)\\n);\\n$configure_type(\\n\\t'authorization',\\n\\tarray(\\n\\t\\t'name' => 'Parental authorization',\\n\\t\\t'description' => 'Signed parental authorization used for a fictitious youth member.',\\n\\t\\t'has_expiration' => 0,\\n\\t\\t'has_document_date' => 1,\\n\\t)\\n);\\n$configure_type(\\n\\t'certificate',\\n\\tarray(\\n\\t\\t'name' => 'Medical certificate',\\n\\t\\t'description' => 'Example medical or fitness certificate for demonstration purposes.',\\n\\t\\t'has_expiration' => 1,\\n\\t\\t'has_document_date' => 1,\\n\\t)\\n);\\n$configure_type(\\n\\t'proof-of-address',\\n\\tarray(\\n\\t\\t'name' => 'Proof of address',\\n\\t\\t'description' => 'Example address document. No real address is stored in this demo.',\\n\\t\\t'has_expiration' => 0,\\n\\t\\t'has_document_date' => 1,\\n\\t)\\n);\\n$configure_type(\\n\\t'photo',\\n\\tarray(\\n\\t\\t'name' => 'Profile photo',\\n\\t\\t'description' => 'Example profile photo requirement. The demo uses a neutral PDF placeholder.',\\n\\t\\t'has_expiration' => 0,\\n\\t\\t'has_document_date' => 0,\\n\\t)\\n);\\n$configure_type(\\n\\t'signed-form',\\n\\tarray(\\n\\t\\t'name' => 'Signed membership form',\\n\\t\\t'description' => 'Example signed registration or volunteer form.',\\n\\t\\t'has_expiration' => 0,\\n\\t\\t'has_document_date' => 1,\\n\\t)\\n);\\n\\n$type_ids = array();\\nforeach ( $repository->get_document_types() as $type ) {\\n\\t$type_ids[ $type->slug ] = (int) $type->id;\\n}\\n\\n$adult_template = $repository->save_template(\\n\\tarray(\\n\\t\\t'name' => 'Adult member registration',\\n\\t\\t'description' => 'Typical dossier for an adult member joining a sports club.',\\n\\t),\\n\\tarray(\\n\\t\\tarray( 'document_type_id' => $type_ids['identity-document'], 'required' => 1, 'sort_order' => 0 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['proof-of-address'],   'required' => 1, 'sort_order' => 1 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['photo'],              'required' => 0, 'sort_order' => 2 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['signed-form'],        'required' => 1, 'sort_order' => 3 ),\\n\\t)\\n);\\n\\n$youth_template = $repository->save_template(\\n\\tarray(\\n\\t\\t'name' => 'Youth member registration',\\n\\t\\t'description' => 'Typical dossier for a youth athlete and parental documents.',\\n\\t),\\n\\tarray(\\n\\t\\tarray( 'document_type_id' => $type_ids['authorization'],     'required' => 1, 'sort_order' => 0 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['certificate'],       'required' => 1, 'sort_order' => 1 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['photo'],             'required' => 0, 'sort_order' => 2 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['signed-form'],       'required' => 1, 'sort_order' => 3 ),\\n\\t)\\n);\\n\\n$volunteer_template = $repository->save_template(\\n\\tarray(\\n\\t\\t'name' => 'Volunteer onboarding',\\n\\t\\t'description' => 'Simple example dossier for a volunteer joining an association.',\\n\\t),\\n\\tarray(\\n\\t\\tarray( 'document_type_id' => $type_ids['identity-document'], 'required' => 1, 'sort_order' => 0 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['signed-form'],       'required' => 1, 'sort_order' => 1 ),\\n\\t\\tarray( 'document_type_id' => $type_ids['photo'],             'required' => 0, 'sort_order' => 2 ),\\n\\t)\\n);\\n\\n$dossiers = array();\\n$dossiers['emma']   = $repository->assign_template( $emma_id,   $adult_template );\\n$dossiers['liam']   = $repository->assign_template( $liam_id,   $adult_template );\\n$dossiers['sofia']  = $repository->assign_template( $sofia_id,  $youth_template );\\n$dossiers['noah']   = $repository->assign_template( $noah_id,   $youth_template );\\n$dossiers['maya']   = $repository->assign_template( $maya_id,   $volunteer_template );\\n$dossiers['oliver'] = $repository->assign_template( $oliver_id, $volunteer_template );\\n\\n$storage_ready = Sprint_Collect_Storage::ensure_base_directory();\\n$demo_pdf = base64_decode( 'JVBERi0xLjMKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgKG9wZW5zb3VyY2UpCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUgo+PgplbmRvYmoKMiAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYSAvRW5jb2RpbmcgL1dpbkFuc2lFbmNvZGluZyAvTmFtZSAvRjEgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250Cj4+CmVuZG9iagozIDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLUJvbGQgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YyIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKNCAwIG9iago8PAovQ29udGVudHMgOCAwIFIgL01lZGlhQm94IFsgMCAwIDU5NS4yNzU2IDg0MS44ODk4IF0gL1BhcmVudCA3IDAgUiAvUmVzb3VyY2VzIDw8Ci9Gb250IDEgMCBSIC9Qcm9jU2V0IFsgL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdCj4+IC9Sb3RhdGUgMCAvVHJhbnMgPDwKCj4+IAogIC9UeXBlIC9QYWdlCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9QYWdlTW9kZSAvVXNlTm9uZSAvUGFnZXMgNyAwIFIgL1R5cGUgL0NhdGFsb2cKPj4KZW5kb2JqCjYgMCBvYmoKPDwKL0F1dGhvciAoYW5vbnltb3VzKSAvQ3JlYXRpb25EYXRlIChEOjIwMjYwODI3MTM0MzQ5KzAwJzAwJykgL0NyZWF0b3IgKGFub255bW91cykgL0tleXdvcmRzICgpIC9Nb2REYXRlIChEOjIwMjYwODI3MTM0MzQ5KzAwJzAwJykgL1Byb2R1Y2VyIChSZXBvcnRMYWIgUERGIExpYnJhcnkgLSBcKG9wZW5zb3VyY2VcKSkgCiAgL1N1YmplY3QgKHVuc3BlY2lmaWVkKSAvVGl0bGUgKHVudGl0bGVkKSAvVHJhcHBlZCAvRmFsc2UKPj4KZW5kb2JqCjcgMCBvYmoKPDwKL0NvdW50IDEgL0tpZHMgWyA0IDAgUiBdIC9UeXBlIC9QYWdlcwo+PgplbmRvYmoKOCAwIG9iago8PAovRmlsdGVyIFsgL0FTQ0lJODVEZWNvZGUgL0ZsYXRlRGVjb2RlIF0gL0xlbmd0aCAyOTYKPj4Kc3RyZWFtCkdhczMvNCY8YUonRiFGLl5aKDNmJjdXLFVBWWxhSTZSPTpnWVNiRFA\\\/LmxyWCskQk5jLTorSTorQTgkaCk4OjAnTDovLV1oVD5rQyFuVEw8YFpdWjQ3Qy5NaF5mSlwhUzpgRTZyM2tRSzNWdC4oKjM0MmlpR2U8KFVAQio\\\/WjwiI2JCP2xqUmY4ZTQ\\\/LlNma2ckI3RYLjNtImRsTlVtISNxLj5lNjdWLmJtUGxzTj48c2gpWmpGYkdjYEdRYz84ZSZgP0hSaD04OEIjY1k7ODQlSmYpLi1gYTYrKmclT1JsLjxmYVtHNS9AbyxRNTJkLDcodGpEWUMwMHMoJlU6KDQrWi4xa3NGSzBUVT4mYltfKmBKbjIyKEpTOjlLPF9YJXFhZlBzVX4+ZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgOQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwNjEgMDAwMDAgbiAKMDAwMDAwMDEwMiAwMDAwMCBuIAowMDAwMDAwMjA5IDAwMDAwIG4gCjAwMDAwMDAzMjEgMDAwMDAgbiAKMDAwMDAwMDUyNCAwMDAwMCBuIAowMDAwMDAwNTkyIDAwMDAwIG4gCjAwMDAwMDA4NTMgMDAwMDAgbiAKMDAwMDAwMDkxMiAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9JRCAKWzw2OWI4MTE5OTZmNmE1YTBmY2Y1MWIyODUyM2Y4ZDM5Yz48NjliODExOTk2ZjZhNWEwZmNmNTFiMjg1MjNmOGQzOWM+XQolIFJlcG9ydExhYiBnZW5lcmF0ZWQgUERGIGRvY3VtZW50IC0tIGRpZ2VzdCAob3BlbnNvdXJjZSkKCi9JbmZvIDYgMCBSCi9Sb290IDUgMCBSCi9TaXplIDkKPj4Kc3RhcnR4cmVmCjEyOTgKJSVFT0YK' );\\n\\n$find_document = static function( $dossier_id, $slug ) use ( $wpdb ) {\\n\\t$docs  = $wpdb->prefix . 'sprint_collect_user_documents';\\n\\t$types = $wpdb->prefix . 'sprint_collect_document_types';\\n\\treturn $wpdb->get_row(\\n\\t\\t$wpdb->prepare(\\n\\t\\t\\t'SELECT ud.*, dt.slug FROM %i ud INNER JOIN %i dt ON dt.id = ud.document_type_id WHERE ud.dossier_id = %d AND dt.slug = %s LIMIT 1',\\n\\t\\t\\t$docs,\\n\\t\\t\\t$types,\\n\\t\\t\\tabsint( $dossier_id ),\\n\\t\\t\\t$slug\\n\\t\\t)\\n\\t);\\n};\\n\\n$set_document = static function( $dossier_id, $slug, $status, $args = array() ) use ( $wpdb, $repository, $admin_id, $now, $demo_pdf, $find_document ) {\\n\\t$doc = $find_document( $dossier_id, $slug );\\n\\tif ( ! $doc ) {\\n\\t\\treturn 0;\\n\\t}\\n\\n\\t$needs_file = in_array( $status, array( 'received', 'correction', 'validated', 'expired' ), true );\\n\\t$data = array(\\n\\t\\t'status'        => $status,\\n\\t\\t'admin_comment' => isset( $args['comment'] ) ? sanitize_textarea_field( $args['comment'] ) : '',\\n\\t\\t'expires_at'    => ! empty( $args['expires_at'] ) ? $args['expires_at'] : null,\\n\\t\\t'document_date' => ! empty( $args['document_date'] ) ? $args['document_date'] : null,\\n\\t\\t'updated_at'    => $now,\\n\\t\\t'validated_at'  => 'validated' === $status ? $now : null,\\n\\t\\t'validated_by'  => 'validated' === $status ? $admin_id : null,\\n\\t);\\n\\n\\tif ( $needs_file ) {\\n\\t\\t$base = Sprint_Collect_Storage::get_base_directory();\\n\\t\\t$relative_dir = absint( $doc->user_id ) . '\\\/' . absint( $dossier_id );\\n\\t\\t$target_dir = trailingslashit( $base ) . $relative_dir;\\n\\t\\twp_mkdir_p( $target_dir );\\n\\t\\t$safe_slug = sanitize_file_name( $slug );\\n\\t\\t$filename = 'demo-' . $safe_slug . '-' . absint( $doc->id ) . '.pdf';\\n\\t\\t$target = trailingslashit( $target_dir ) . $filename;\\n\\t\\tfile_put_contents( $target, $demo_pdf );\\n\\t\\t@chmod( $target, 0640 );\\n\\n\\t\\t$data['file_path']     = $relative_dir . '\\\/' . $filename;\\n\\t\\t$data['original_name'] = 'demo-' . $safe_slug . '.pdf';\\n\\t\\t$data['mime_type']     = 'application\\\/pdf';\\n\\t\\t$data['file_size']     = is_file( $target ) ? filesize( $target ) : strlen( $demo_pdf );\\n\\t\\t$data['uploaded_at']   = ! empty( $args['uploaded_at'] ) ? $args['uploaded_at'] : $now;\\n\\t}\\n\\n\\t$wpdb->update(\\n\\t\\t$wpdb->prefix . 'sprint_collect_user_documents',\\n\\t\\t$data,\\n\\t\\tarray( 'id' => absint( $doc->id ) )\\n\\t);\\n\\n\\tif ( $needs_file ) {\\n\\t\\t$repository->log(\\n\\t\\t\\t$dossier_id,\\n\\t\\t\\tabsint( $doc->id ),\\n\\t\\t\\tabsint( $doc->user_id ),\\n\\t\\t\\t'document_uploaded',\\n\\t\\t\\tarray( 'original_name' => $data['original_name'], 'demo' => true )\\n\\t\\t);\\n\\t}\\n\\n\\tif ( 'validated' === $status || 'correction' === $status || 'expired' === $status ) {\\n\\t\\t$repository->log(\\n\\t\\t\\t$dossier_id,\\n\\t\\t\\tabsint( $doc->id ),\\n\\t\\t\\t$admin_id,\\n\\t\\t\\t'document_status_changed',\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'status'  => $status,\\n\\t\\t\\t\\t'comment' => $data['admin_comment'],\\n\\t\\t\\t\\t'demo'    => true,\\n\\t\\t\\t)\\n\\t\\t);\\n\\t}\\n\\treturn absint( $doc->id );\\n};\\n\\n$upload_time_recent = date( 'Y-m-d H:i:s', strtotime( '-2 days', $today_ts ) );\\n$upload_time_week   = date( 'Y-m-d H:i:s', strtotime( '-7 days', $today_ts ) );\\n$upload_time_old    = date( 'Y-m-d H:i:s', strtotime( '-20 days', $today_ts ) );\\n\\n\\\/\\\/ Emma Carter: complete adult dossier.\\n$set_document( $dossiers['emma'], 'identity-document', 'validated', array( 'expires_at' => $plus_365, 'document_date' => $minus_30, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['emma'], 'proof-of-address', 'validated', array( 'document_date' => $minus_30, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['emma'], 'photo', 'validated', array( 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['emma'], 'signed-form', 'validated', array( 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n\\n\\\/\\\/ Liam Wilson: mixed state, including a document awaiting review.\\n$set_document( $dossiers['liam'], 'identity-document', 'received', array( 'expires_at' => $plus_365, 'document_date' => $minus_14, 'uploaded_at' => $upload_time_recent ) );\\n$set_document( $dossiers['liam'], 'proof-of-address', 'validated', array( 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['liam'], 'photo', 'missing' );\\n$set_document( $dossiers['liam'], 'signed-form', 'missing' );\\n\\n\\\/\\\/ Sofia Martin: correction requested.\\n$set_document( $dossiers['sofia'], 'authorization', 'validated', array( 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['sofia'], 'certificate', 'correction', array( 'expires_at' => $plus_180, 'document_date' => $minus_30, 'uploaded_at' => $upload_time_recent, 'comment' => 'Please upload a fully readable copy including the signature.' ) );\\n$set_document( $dossiers['sofia'], 'photo', 'validated', array( 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['sofia'], 'signed-form', 'validated', array( 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n\\n\\\/\\\/ Noah Brown: expired certificate and one missing form.\\n$set_document( $dossiers['noah'], 'authorization', 'validated', array( 'document_date' => $minus_30, 'uploaded_at' => $upload_time_old ) );\\n$set_document( $dossiers['noah'], 'certificate', 'expired', array( 'expires_at' => $minus_3, 'document_date' => $minus_30, 'uploaded_at' => $upload_time_old, 'comment' => 'This certificate has expired. A renewed document is required.' ) );\\n$set_document( $dossiers['noah'], 'photo', 'validated', array( 'uploaded_at' => $upload_time_old ) );\\n$set_document( $dossiers['noah'], 'signed-form', 'missing' );\\n\\n\\\/\\\/ Maya Lee: complete volunteer dossier, with an identity document expiring soon.\\n$set_document( $dossiers['maya'], 'identity-document', 'validated', array( 'expires_at' => $plus_12, 'document_date' => $minus_30, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['maya'], 'signed-form', 'validated', array( 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['maya'], 'photo', 'validated', array( 'uploaded_at' => $upload_time_week ) );\\n\\n\\\/\\\/ Oliver King: one validated document and one awaiting review.\\n$set_document( $dossiers['oliver'], 'identity-document', 'validated', array( 'expires_at' => $plus_60, 'document_date' => $minus_14, 'uploaded_at' => $upload_time_week ) );\\n$set_document( $dossiers['oliver'], 'signed-form', 'received', array( 'document_date' => $minus_7, 'uploaded_at' => $upload_time_recent ) );\\n$set_document( $dossiers['oliver'], 'photo', 'missing' );\\n\\nforeach ( $dossiers as $dossier_id ) {\\n\\tif ( $dossier_id ) {\\n\\t\\t$repository->refresh_dossier_status( $dossier_id );\\n\\t}\\n}\\n\\n\\\/\\\/ Simulate one previous reminder without sending email.\\nif ( ! empty( $dossiers['noah'] ) ) {\\n\\t$wpdb->update(\\n\\t\\t$wpdb->prefix . 'sprint_collect_dossiers',\\n\\t\\tarray( 'last_reminder_at' => date( 'Y-m-d H:i:s', strtotime( '-8 days', $today_ts ) ), 'updated_at' => $now ),\\n\\t\\tarray( 'id' => absint( $dossiers['noah'] ) )\\n\\t);\\n\\t$repository->log(\\n\\t\\t$dossiers['noah'],\\n\\t\\tnull,\\n\\t\\t$admin_id,\\n\\t\\t'reminder_sent',\\n\\t\\tarray(\\n\\t\\t\\t'source'  => 'manual',\\n\\t\\t\\t'reasons' => array( 'missing' => 1, 'correction' => 0, 'expired' => 1, 'expiring' => 0 ),\\n\\t\\t\\t'demo'    => true,\\n\\t\\t)\\n\\t);\\n}\\n\\n\\\/\\\/ Create a front-end page preconfigured to show Liam's fictitious member portal.\\n$portal_page_id = wp_insert_post(\\n\\tarray(\\n\\t\\t'post_title'   => 'Supporting documents \\u2013 Demo member',\\n\\t\\t'post_name'    => 'supporting-documents-demo',\\n\\t\\t'post_content' => '[sprint_collect user_id=\\\"' . absint( $liam_id ) . '\\\"]',\\n\\t\\t'post_status'  => 'publish',\\n\\t\\t'post_type'    => 'page',\\n\\t),\\n\\ttrue\\n);\\nif ( ! is_wp_error( $portal_page_id ) && $portal_page_id ) {\\n\\t$settings = get_option( 'sprint_collect_settings', array() );\\n\\t$settings = is_array( $settings ) ? $settings : array();\\n\\t$settings['portal_page_id'] = absint( $portal_page_id );\\n\\tupdate_option( 'sprint_collect_settings', $settings, false );\\n}\\n\\n\\\/\\\/ Add a second page explaining the demo and linking to the member portal.\\n$guide_content  = '<h2>Sprint Collect demonstration<\\\/h2>';\\n$guide_content .= '<p><strong>All names, email addresses and documents in this preview are fictitious.<\\\/strong><\\\/p>';\\n$guide_content .= '<p>This temporary WordPress Playground site demonstrates a sports-club workflow with complete, incomplete, received, correction-required and expired documents.<\\\/p>';\\nif ( ! is_wp_error( $portal_page_id ) && $portal_page_id ) {\\n\\t$guide_content .= '<p><a href=\\\"' . esc_url( get_permalink( $portal_page_id ) ) . '\\\">Open the demo member portal<\\\/a><\\\/p>';\\n}\\nwp_insert_post(\\n\\tarray(\\n\\t\\t'post_title'   => 'Sprint Collect Demo Guide',\\n\\t\\t'post_name'    => 'sprint-collect-demo-guide',\\n\\t\\t'post_content' => $guide_content,\\n\\t\\t'post_status'  => 'publish',\\n\\t\\t'post_type'    => 'page',\\n\\t)\\n);\\n\\n\\\/\\\/ Disable outbound email in the temporary demo and display a clear demo notice.\\nif ( defined( 'WPMU_PLUGIN_DIR' ) ) {\\n\\twp_mkdir_p( WPMU_PLUGIN_DIR );\\n\\t$mu_code = <<<'PHP'\\n<?php\\n\\\/**\\n * Sprint Collect WordPress.org Playground demo guard.\\n *\\\/\\nadd_filter(\\n\\t'pre_wp_mail',\\n\\tstatic function( $return, $atts ) {\\n\\t\\tunset( $atts );\\n\\t\\treturn true;\\n\\t},\\n\\t10,\\n\\t2\\n);\\nadd_action(\\n\\t'admin_notices',\\n\\tstatic function() {\\n\\t\\tif ( ! isset( $_GET['page'] ) || 'sprint-collect' !== sanitize_key( wp_unslash( $_GET['page'] ) ) ) {\\n\\t\\t\\treturn;\\n\\t\\t}\\n\\t\\techo '<div class=\\\"notice notice-info\\\"><p><strong>Sprint Collect demo:<\\\/strong> all people, email addresses and documents are fictitious. Outbound email is disabled in this preview.<\\\/p><\\\/div>';\\n\\t}\\n);\\nPHP;\\n\\tfile_put_contents( trailingslashit( WPMU_PLUGIN_DIR ) . 'sprint-collect-demo-guard.php', $mu_code );\\n}\\n\\nupdate_option(\\n\\t'sprint_collect_demo_overview',\\n\\tarray(\\n\\t\\t'note'           => 'All demonstration content is fictitious and contains no real personal data.',\\n\\t\\t'portal_page_id' => is_wp_error( $portal_page_id ) ? 0 : absint( $portal_page_id ),\\n\\t\\t'demo_user_id'   => absint( $liam_id ),\\n\\t),\\n\\tfalse\\n);\\n\\nupdate_option( 'sprint_collect_demo_seed_v1', gmdate( 'c' ), false );\\nflush_rewrite_rules( false );\\n?>\"},{\"step\":\"login\",\"username\":\"admin\",\"password\":\"password\"}]}"}},"all_blocks":[],"tagged_versions":["1.0.0","1.0.1"],"block_files":[],"assets_screenshots":{"screenshot-1-fr_FR.png":{"filename":"screenshot-1-fr_FR.png","revision":3668959,"resolution":"1","location":"assets","locale":"fr_FR","width":1440,"height":900},"screenshot-1.png":{"filename":"screenshot-1.png","revision":3668959,"resolution":"1","location":"assets","locale":"","width":1440,"height":900},"screenshot-2-fr_FR.png":{"filename":"screenshot-2-fr_FR.png","revision":3668959,"resolution":"2","location":"assets","locale":"fr_FR","width":1440,"height":900},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3668959,"resolution":"2","location":"assets","locale":"","width":1440,"height":900},"screenshot-3-fr_FR.png":{"filename":"screenshot-3-fr_FR.png","revision":3668959,"resolution":"3","location":"assets","locale":"fr_FR","width":1440,"height":900},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3668959,"resolution":"3","location":"assets","locale":"","width":1440,"height":900},"screenshot-4-fr_FR.png":{"filename":"screenshot-4-fr_FR.png","revision":3668959,"resolution":"4","location":"assets","locale":"fr_FR","width":1440,"height":900},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3668959,"resolution":"4","location":"assets","locale":"","width":1440,"height":900},"screenshot-5-fr_FR.png":{"filename":"screenshot-5-fr_FR.png","revision":3668959,"resolution":"5","location":"assets","locale":"fr_FR","width":1440,"height":900},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3668959,"resolution":"5","location":"assets","locale":"","width":1440,"height":900}},"screenshots":{"1":"Dashboard showing active dossiers, documents awaiting review, expirations, and reminders.","2":"Dossier list with member, template, completion, and validation filters.","3":"Document review workflow with validation, correction requests, comments, and dates.","4":"Secure member upload portal for requested supporting documents.","5":"Display modes, notifications, maintenance, and WordPress privacy integration."}},"plugin_section":[],"plugin_tags":[141027,9054,396,85,1917],"plugin_category":[54],"plugin_contributors":[274079],"plugin_business_model":[],"class_list":["post-359489","plugin","type-plugin","status-publish","hentry","plugin_tags-associations","plugin_tags-documents","plugin_tags-privacy","plugin_tags-uploads","plugin_tags-users","plugin_category-security-and-spam-protection","plugin_contributors-sprintplugins","plugin_committers-sprintplugins"],"banners":{"banner":"https:\/\/ps.w.org\/sprint-collect\/assets\/banner-772x250.png?rev=3668959","banner_2x":"https:\/\/ps.w.org\/sprint-collect\/assets\/banner-1544x500.png?rev=3668959","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/sprint-collect\/assets\/icon.svg?rev=3668959","icon":"https:\/\/ps.w.org\/sprint-collect\/assets\/icon.svg?rev=3668959","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/sprint-collect\/assets\/screenshot-1.png?rev=3668959","caption":"Dashboard showing active dossiers, documents awaiting review, expirations, and reminders."},{"src":"https:\/\/ps.w.org\/sprint-collect\/assets\/screenshot-2.png?rev=3668959","caption":"Dossier list with member, template, completion, and validation filters."},{"src":"https:\/\/ps.w.org\/sprint-collect\/assets\/screenshot-3.png?rev=3668959","caption":"Document review workflow with validation, correction requests, comments, and dates."},{"src":"https:\/\/ps.w.org\/sprint-collect\/assets\/screenshot-4.png?rev=3668959","caption":"Secure member upload portal for requested supporting documents."},{"src":"https:\/\/ps.w.org\/sprint-collect\/assets\/screenshot-5.png?rev=3668959","caption":"Display modes, notifications, maintenance, and WordPress privacy integration."}],"raw_content":"<!--section=description-->\n<p>Sprint Collect provides a complete document collection workflow for clubs, associations, and other WordPress sites.<\/p>\n\n<p>The free plugin includes:<\/p>\n\n<ul>\n<li>Reusable document types and dossier templates.<\/li>\n<li>Private file storage outside the public Media Library.<\/li>\n<li>A secure front-end upload portal for logged-in users and authorized representatives.<\/li>\n<li>Administrative review, validation, correction requests, comments, dates, and expiration tracking.<\/li>\n<li>Individual dossier assignment, closure, reopening, purging, and deletion.<\/li>\n<li>Manual reminders and standard notification emails.<\/li>\n<li>A basic activity history and maintenance tools.<\/li>\n<li>Personal data export and erasure integration with the WordPress privacy tools.<\/li>\n<li>Essential, Full, and Expert interface modes, including optional inheritance from Sprint Club.<\/li>\n<li>A documented public API for compatible add-ons.<\/li>\n<\/ul>\n\n<p>Sprint Collect works on its own and does not require an external service. Uploaded files remain on the WordPress site in a protected private directory.<\/p>\n\n<p>An optional, separately distributed Sprint Collect Pro add-on can provide automations, advanced conditions, lifecycle workflows, exports, integrations, email customization, and granular permissions. Premium implementation code is not included in this WordPress.org package.<\/p>\n\n<h3>Privacy<\/h3>\n\n<p>Sprint Collect stores dossier records, uploaded file metadata, validation information, administrative comments, dates, and private document files. These records can contain sensitive personal data selected by the site administrator.<\/p>\n\n<p>The plugin adds suggested text to the WordPress privacy policy guide and integrates with the WordPress personal data exporter and eraser. Site administrators remain responsible for defining lawful collection purposes, access rules, and retention periods.<\/p>\n\n<p>Sprint Collect does not contact an external server by itself. Optional add-ons may have their own privacy disclosures.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin ZIP from Plugins &gt; Add New, or install it from WordPress.org.<\/li>\n<li>Activate Sprint Collect.<\/li>\n<li>Open Sprint Collect and configure document types and templates.<\/li>\n<li>Create or select a portal page containing the <code>[sprint_collect]<\/code> shortcode.<\/li>\n<li>Assign templates to WordPress users.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"where%20are%20uploaded%20documents%20stored%3F\"><h3>Where are uploaded documents stored?<\/h3><\/dt>\n<dd><p>Files are stored in a protected private directory under the WordPress uploads directory. They are not added to the public Media Library.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20require%20sprint%20club%3F\"><h3>Does the plugin require Sprint Club?<\/h3><\/dt>\n<dd><p>No. Sprint Club is optional. When installed, it can provide centralized navigation and interface-mode inheritance.<\/p><\/dd>\n<dt id=\"does%20the%20free%20plugin%20expire%20or%20impose%20a%20quota%3F\"><h3>Does the free plugin expire or impose a quota?<\/h3><\/dt>\n<dd><p>No. The free features are complete and do not expire. Sprint Collect Pro is a separate optional add-on.<\/p><\/dd>\n<dt id=\"are%20files%20removed%20when%20the%20plugin%20is%20uninstalled%3F\"><h3>Are files removed when the plugin is uninstalled?<\/h3><\/dt>\n<dd><p>Not by default. Administrators can explicitly enable permanent data removal before uninstalling the plugin.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Fixed the activation flow for premium add-ons so they can be activated before their independent license is entered.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial WordPress.org release.<\/li>\n<li>Secure private document collection, review, validation, and reminders.<\/li>\n<li>Added WordPress personal data export and erasure support.<\/li>\n<li>Added Essential, Full, and Expert interface modes with Sprint Club integration.<\/li>\n<\/ul>","raw_excerpt":"Securely collect, review, and manage supporting documents for WordPress users.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/359489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=359489"}],"author":[{"embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/sprintplugins"}],"wp:attachment":[{"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=359489"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=359489"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=359489"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=359489"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=359489"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/bal.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=359489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}