feat(07-01): implement complete job management workflow

Adds comprehensive job submission, editing, and deactivation functionality with proper form handling and permissions. Includes administrator capabilities for job_offer management and fixed dashboard navigation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 19:49:21 +09:00
parent 1ed164aed0
commit 737f3d6fe9
4 changed files with 695 additions and 7 deletions

View File

@@ -161,6 +161,9 @@ class DDHH_JM_Formidable {
// Hook into Formidable form submission
add_action( 'frm_after_create_entry', array( __CLASS__, 'handle_registration_submission' ), 30, 2 );
add_action( 'frm_after_create_entry', array( __CLASS__, 'handle_job_submission' ), 30, 2 );
add_action( 'frm_after_create_entry', array( __CLASS__, 'handle_job_edit_submission' ), 30, 2 );
add_action( 'frm_after_create_entry', array( __CLASS__, 'handle_job_deactivation_submission' ), 30, 2 );
// Add JavaScript redirect for registration form
add_action( 'wp_footer', array( __CLASS__, 'add_registration_redirect_script' ) );
@@ -168,6 +171,9 @@ class DDHH_JM_Formidable {
// Hook into Formidable form validation for ownership check
add_filter( 'frm_validate_entry', array( __CLASS__, 'validate_job_ownership' ), 10, 2 );
add_filter( 'frm_validate_entry', array( __CLASS__, 'validate_job_deactivation_ownership' ), 10, 2 );
// Hook to pre-populate edit form fields
add_filter( 'frm_get_default_value', array( __CLASS__, 'prepopulate_edit_form_fields' ), 10, 3 );
}
/**
@@ -437,6 +443,324 @@ class DDHH_JM_Formidable {
do_action( 'ddhh_provider_registered', $user_id, $organization_name );
}
/**
* Handle job submission form entry
*
* @param int $entry_id Entry ID.
* @param int $form_id Form ID.
*/
public static function handle_job_submission( $entry_id, $form_id ) {
// Only process our job submission form
if ( $form_id != self::get_job_submission_form_id() ) {
return;
}
error_log( 'DDHH Job Submission: Processing form entry ' . $entry_id );
// Get entry data
$entry = FrmEntry::getOne( $entry_id, true );
if ( ! $entry ) {
error_log( 'DDHH Job Submission: Failed to get entry' );
return;
}
// Extract field values
$job_title = '';
$job_description = '';
$job_location = '';
$job_type = '';
$job_deadline = '';
$job_contact_email = '';
$job_logo = '';
foreach ( $entry->metas as $field_id => $value ) {
$field = FrmField::getOne( $field_id );
if ( ! $field ) {
continue;
}
switch ( $field->field_key ) {
case 'job_title':
$job_title = sanitize_text_field( $value );
break;
case 'job_description':
$job_description = wp_kses_post( $value );
break;
case 'job_location':
$job_location = sanitize_text_field( $value );
break;
case 'job_type':
$job_type = sanitize_text_field( $value );
break;
case 'job_deadline':
$job_deadline = sanitize_text_field( $value );
break;
case 'job_contact_email':
$job_contact_email = sanitize_email( $value );
break;
case 'job_logo':
$job_logo = $value; // File ID
break;
}
}
// Validate required fields
if ( empty( $job_title ) || empty( $job_description ) || empty( $job_location ) || empty( $job_type ) || empty( $job_contact_email ) ) {
error_log( 'DDHH Job Submission: Validation failed - missing required fields' );
return;
}
// Create job_offer post
$post_data = array(
'post_title' => $job_title,
'post_content' => $job_description,
'post_type' => 'job_offer',
'post_status' => 'pending',
'post_author' => get_current_user_id(),
);
$post_id = wp_insert_post( $post_data );
// Check for errors
if ( is_wp_error( $post_id ) ) {
error_log( 'DDHH Job Submission: wp_insert_post failed - ' . $post_id->get_error_message() );
return;
}
error_log( 'DDHH Job Submission: Post created successfully with ID ' . $post_id );
// Save custom fields
update_post_meta( $post_id, 'job_location', $job_location );
update_post_meta( $post_id, 'job_type', $job_type );
update_post_meta( $post_id, 'job_contact_email', $job_contact_email );
if ( ! empty( $job_deadline ) ) {
update_post_meta( $post_id, 'job_deadline', $job_deadline );
}
// Handle logo upload if present
if ( ! empty( $job_logo ) && is_numeric( $job_logo ) ) {
set_post_thumbnail( $post_id, absint( $job_logo ) );
error_log( 'DDHH Job Submission: Logo set as featured image' );
}
error_log( 'DDHH Job Submission: Job offer created successfully' );
do_action( 'ddhh_job_submitted', $post_id, $entry_id );
}
/**
* Handle job edit form submission
*
* @param int $entry_id Entry ID.
* @param int $form_id Form ID.
*/
public static function handle_job_edit_submission( $entry_id, $form_id ) {
// Only process our job edit form
if ( $form_id != self::get_job_edit_form_id() ) {
return;
}
// Check if we have a job_id to update
if ( ! isset( $_GET['job_id'] ) ) {
error_log( 'DDHH Job Edit: No job_id provided' );
return;
}
$post_id = absint( $_GET['job_id'] );
error_log( 'DDHH Job Edit: Processing form entry ' . $entry_id . ' for post ' . $post_id );
// Verify post exists and user owns it
$post = get_post( $post_id );
if ( ! $post || 'job_offer' !== $post->post_type ) {
error_log( 'DDHH Job Edit: Invalid post' );
return;
}
if ( absint( $post->post_author ) !== get_current_user_id() ) {
error_log( 'DDHH Job Edit: User does not own this post' );
return;
}
// Get entry data
$entry = FrmEntry::getOne( $entry_id, true );
if ( ! $entry ) {
error_log( 'DDHH Job Edit: Failed to get entry' );
return;
}
// Extract field values (accounting for "2" suffix)
$job_title = '';
$job_description = '';
$job_location = '';
$job_type = '';
$job_deadline = '';
$job_contact_email = '';
$job_logo = '';
foreach ( $entry->metas as $field_id => $value ) {
$field = FrmField::getOne( $field_id );
if ( ! $field ) {
continue;
}
switch ( $field->field_key ) {
case 'job_title':
case 'job_title2':
$job_title = sanitize_text_field( $value );
break;
case 'job_description':
case 'job_description2':
$job_description = wp_kses_post( $value );
break;
case 'job_location':
case 'job_location2':
$job_location = sanitize_text_field( $value );
break;
case 'job_type':
case 'job_type2':
$job_type = sanitize_text_field( $value );
break;
case 'job_deadline':
case 'job_deadline2':
$job_deadline = sanitize_text_field( $value );
break;
case 'job_contact_email':
case 'job_contact_email2':
$job_contact_email = sanitize_email( $value );
break;
case 'job_logo':
case 'job_logo2':
$job_logo = $value; // File ID
break;
}
}
// Validate required fields
if ( empty( $job_title ) || empty( $job_description ) || empty( $job_location ) || empty( $job_type ) || empty( $job_contact_email ) ) {
error_log( 'DDHH Job Edit: Validation failed - missing required fields' );
return;
}
// Update job_offer post
$post_data = array(
'ID' => $post_id,
'post_title' => $job_title,
'post_content' => $job_description,
'post_status' => 'pending', // Reset to pending for admin review
);
$result = wp_update_post( $post_data );
// Check for errors
if ( is_wp_error( $result ) ) {
error_log( 'DDHH Job Edit: wp_update_post failed - ' . $result->get_error_message() );
return;
}
error_log( 'DDHH Job Edit: Post updated successfully with ID ' . $post_id );
// Update custom fields
update_post_meta( $post_id, 'job_location', $job_location );
update_post_meta( $post_id, 'job_type', $job_type );
update_post_meta( $post_id, 'job_contact_email', $job_contact_email );
if ( ! empty( $job_deadline ) ) {
update_post_meta( $post_id, 'job_deadline', $job_deadline );
}
// Handle logo upload if present
if ( ! empty( $job_logo ) && is_numeric( $job_logo ) ) {
set_post_thumbnail( $post_id, absint( $job_logo ) );
error_log( 'DDHH Job Edit: Logo updated' );
}
error_log( 'DDHH Job Edit: Job offer updated successfully' );
do_action( 'ddhh_job_edited', $post_id, $entry_id );
}
/**
* Handle job deactivation form submission
*
* @param int $entry_id Entry ID.
* @param int $form_id Form ID.
*/
public static function handle_job_deactivation_submission( $entry_id, $form_id ) {
// Only process our job deactivation form
if ( $form_id != self::get_job_deactivation_form_id() ) {
return;
}
// Check if we have a job_id to deactivate
if ( ! isset( $_GET['job_id'] ) ) {
error_log( 'DDHH Job Deactivation: No job_id provided' );
return;
}
$post_id = absint( $_GET['job_id'] );
error_log( 'DDHH Job Deactivation: Processing form entry ' . $entry_id . ' for post ' . $post_id );
// Verify post exists and user owns it
$post = get_post( $post_id );
if ( ! $post || 'job_offer' !== $post->post_type ) {
error_log( 'DDHH Job Deactivation: Invalid post' );
return;
}
if ( absint( $post->post_author ) !== get_current_user_id() ) {
error_log( 'DDHH Job Deactivation: User does not own this post' );
return;
}
// Get entry data
$entry = FrmEntry::getOne( $entry_id, true );
if ( ! $entry ) {
error_log( 'DDHH Job Deactivation: Failed to get entry' );
return;
}
// Extract deactivation reason
$deactivation_reason = '';
foreach ( $entry->metas as $field_id => $value ) {
$field = FrmField::getOne( $field_id );
if ( ! $field ) {
continue;
}
if ( 'deactivation_reason' === $field->field_key ) {
$deactivation_reason = sanitize_textarea_field( $value );
break;
}
}
// Update post status to draft (deactivated)
$result = wp_update_post( array(
'ID' => $post_id,
'post_status' => 'draft',
) );
// Check for errors
if ( is_wp_error( $result ) ) {
error_log( 'DDHH Job Deactivation: wp_update_post failed - ' . $result->get_error_message() );
return;
}
error_log( 'DDHH Job Deactivation: Post status updated to draft for post ' . $post_id );
// Save deactivation reason (using ACF field name)
if ( ! empty( $deactivation_reason ) ) {
update_post_meta( $post_id, 'job_deactivation_reason', $deactivation_reason );
update_post_meta( $post_id, 'job_deactivation_date', current_time( 'mysql' ) );
error_log( 'DDHH Job Deactivation: Saved deactivation reason' );
}
error_log( 'DDHH Job Deactivation: Job deactivated successfully' );
do_action( 'ddhh_job_deactivated', $post_id, $entry_id );
}
/**
* Create the job submission form programmatically if it doesn't exist
*/
@@ -777,6 +1101,71 @@ class DDHH_JM_Formidable {
}
}
/**
* Pre-populate edit form fields with existing post data
*
* @param mixed $default_value The default value.
* @param object $field The field object.
* @param bool $dynamic_default Whether to use dynamic default.
* @return mixed The modified default value.
*/
public static function prepopulate_edit_form_fields( $default_value, $field, $dynamic_default ) {
// Only process for the job edit form
if ( absint( $field->form_id ) !== self::get_job_edit_form_id() ) {
return $default_value;
}
// Check if we're editing - look for job_id in URL
if ( ! isset( $_GET['job_id'] ) ) {
return $default_value;
}
$post_id = absint( $_GET['job_id'] );
$post = get_post( $post_id );
if ( ! $post || 'job_offer' !== $post->post_type ) {
return $default_value;
}
// Map field keys to post data (handle both with and without "2" suffix)
switch ( $field->field_key ) {
case 'job_title':
case 'job_title2':
return $post->post_title;
case 'job_description':
case 'job_description2':
return $post->post_content;
case 'job_location':
case 'job_location2':
$value = get_post_meta( $post_id, 'job_location', true );
return $value ? $value : $default_value;
case 'job_type':
case 'job_type2':
$value = get_post_meta( $post_id, 'job_type', true );
return $value ? $value : $default_value;
case 'job_deadline':
case 'job_deadline2':
$value = get_post_meta( $post_id, 'job_deadline', true );
return $value ? $value : $default_value;
case 'job_contact_email':
case 'job_contact_email2':
$value = get_post_meta( $post_id, 'job_contact_email', true );
return $value ? $value : $default_value;
case 'job_logo':
case 'job_logo2':
$value = get_post_thumbnail_id( $post_id );
return $value ? $value : $default_value;
}
return $default_value;
}
/**
* Validate job ownership before allowing edit
*

View File

@@ -50,6 +50,24 @@ class DDHH_JM_Roles {
'manage_options' => false,
)
);
// Grant job_offer capabilities to administrator
$admin_role = get_role( 'administrator' );
if ( $admin_role ) {
$admin_role->add_cap( 'edit_job_offer' );
$admin_role->add_cap( 'read_job_offer' );
$admin_role->add_cap( 'delete_job_offer' );
$admin_role->add_cap( 'edit_job_offers' );
$admin_role->add_cap( 'edit_others_job_offers' );
$admin_role->add_cap( 'publish_job_offers' );
$admin_role->add_cap( 'read_private_job_offers' );
$admin_role->add_cap( 'delete_job_offers' );
$admin_role->add_cap( 'delete_private_job_offers' );
$admin_role->add_cap( 'delete_published_job_offers' );
$admin_role->add_cap( 'delete_others_job_offers' );
$admin_role->add_cap( 'edit_private_job_offers' );
$admin_role->add_cap( 'edit_published_job_offers' );
}
}
/**
@@ -58,5 +76,23 @@ class DDHH_JM_Roles {
*/
public static function remove_roles() {
remove_role( 'ddhh_provider' );
// Remove job_offer capabilities from administrator
$admin_role = get_role( 'administrator' );
if ( $admin_role ) {
$admin_role->remove_cap( 'edit_job_offer' );
$admin_role->remove_cap( 'read_job_offer' );
$admin_role->remove_cap( 'delete_job_offer' );
$admin_role->remove_cap( 'edit_job_offers' );
$admin_role->remove_cap( 'edit_others_job_offers' );
$admin_role->remove_cap( 'publish_job_offers' );
$admin_role->remove_cap( 'read_private_job_offers' );
$admin_role->remove_cap( 'delete_job_offers' );
$admin_role->remove_cap( 'delete_private_job_offers' );
$admin_role->remove_cap( 'delete_published_job_offers' );
$admin_role->remove_cap( 'delete_others_job_offers' );
$admin_role->remove_cap( 'edit_private_job_offers' );
$admin_role->remove_cap( 'edit_published_job_offers' );
}
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* One-time migration script to convert Formidable job submission entries to job_offer posts
*
* Run this file once via: wp eval-file migrate-job-entries.php
*
* @package DDHH_Job_Manager
*/
// Make sure we're running in WordPress context
if ( ! defined( 'ABSPATH' ) ) {
// Try to load WordPress
$wp_load = dirname( __FILE__ ) . '/../../../wp-load.php';
if ( file_exists( $wp_load ) ) {
require_once $wp_load;
} else {
die( 'Could not find WordPress. Please run via WP-CLI: wp eval-file migrate-job-entries.php' );
}
}
// Check if Formidable is available
if ( ! class_exists( 'FrmForm' ) || ! class_exists( 'FrmEntry' ) || ! class_exists( 'FrmField' ) ) {
die( "Error: Formidable Forms is not active.\n" );
}
// Get the job submission form
$form = FrmForm::getOne( 'job_submission' );
if ( ! $form ) {
die( "Error: Job submission form not found.\n" );
}
$form_id = $form->id;
echo "Found job submission form with ID: {$form_id}\n";
// Get all entries for this form using global $wpdb
global $wpdb;
$entry_table = $wpdb->prefix . 'frm_items';
$entry_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT id FROM {$entry_table} WHERE form_id = %d ORDER BY created_at ASC",
$form_id
) );
if ( empty( $entry_ids ) ) {
die( "No entries found to migrate.\n" );
}
// Load full entry objects
$entries = array();
foreach ( $entry_ids as $entry_id ) {
$entry = FrmEntry::getOne( $entry_id );
if ( $entry ) {
$entries[] = $entry;
}
}
echo "Found " . count( $entries ) . " entries to migrate.\n\n";
$migrated = 0;
$skipped = 0;
$errors = 0;
foreach ( $entries as $entry ) {
echo "Processing entry #{$entry->id}...\n";
// Get full entry with meta data
$full_entry = FrmEntry::getOne( $entry->id, true );
if ( ! $full_entry ) {
echo " ERROR: Could not load entry data\n";
$errors++;
continue;
}
// Check if this entry has already been migrated by checking if a post exists
// We'll check if there's a post with the same author and created around the same time
$entry_user_id = absint( $entry->user_id );
if ( empty( $entry_user_id ) ) {
echo " SKIPPED: Entry has no user_id\n";
$skipped++;
continue;
}
// Extract field values
$job_title = '';
$job_description = '';
$job_location = '';
$job_type = '';
$job_deadline = '';
$job_contact_email = '';
$job_logo = '';
foreach ( $full_entry->metas as $field_id => $value ) {
$field = FrmField::getOne( $field_id );
if ( ! $field ) {
continue;
}
switch ( $field->field_key ) {
case 'job_title':
$job_title = sanitize_text_field( $value );
break;
case 'job_description':
$job_description = wp_kses_post( $value );
break;
case 'job_location':
$job_location = sanitize_text_field( $value );
break;
case 'job_type':
$job_type = sanitize_text_field( $value );
break;
case 'job_deadline':
$job_deadline = sanitize_text_field( $value );
break;
case 'job_contact_email':
$job_contact_email = sanitize_email( $value );
break;
case 'job_logo':
$job_logo = $value; // File ID
break;
}
}
// Validate required fields
if ( empty( $job_title ) ) {
echo " SKIPPED: Missing job title\n";
$skipped++;
continue;
}
// Check if a post with this title and author already exists
$existing_post = get_posts( array(
'post_type' => 'job_offer',
'post_status' => 'any',
'author' => $entry_user_id,
'title' => $job_title,
'numberposts' => 1,
) );
if ( ! empty( $existing_post ) ) {
echo " SKIPPED: Post already exists (ID: {$existing_post[0]->ID})\n";
$skipped++;
continue;
}
// Create job_offer post
$post_data = array(
'post_title' => $job_title,
'post_content' => $job_description,
'post_type' => 'job_offer',
'post_status' => 'pending',
'post_author' => $entry_user_id,
'post_date' => $entry->created_at,
);
$post_id = wp_insert_post( $post_data, true );
// Check for errors
if ( is_wp_error( $post_id ) ) {
echo " ERROR: Failed to create post - " . $post_id->get_error_message() . "\n";
$errors++;
continue;
}
// Save custom fields
if ( ! empty( $job_location ) ) {
update_post_meta( $post_id, 'job_location', $job_location );
}
if ( ! empty( $job_type ) ) {
update_post_meta( $post_id, 'job_type', $job_type );
}
if ( ! empty( $job_contact_email ) ) {
update_post_meta( $post_id, 'job_contact_email', $job_contact_email );
}
if ( ! empty( $job_deadline ) ) {
update_post_meta( $post_id, 'job_deadline', $job_deadline );
}
// Handle logo upload if present
if ( ! empty( $job_logo ) && is_numeric( $job_logo ) ) {
set_post_thumbnail( $post_id, absint( $job_logo ) );
}
// Store reference to original entry
update_post_meta( $post_id, '_migrated_from_entry_id', $entry->id );
echo " SUCCESS: Created post #{$post_id} - \"{$job_title}\"\n";
$migrated++;
}
echo "\n=== Migration Complete ===\n";
echo "Migrated: {$migrated}\n";
echo "Skipped: {$skipped}\n";
echo "Errors: {$errors}\n";
echo "Total: " . count( $entries ) . "\n";

View File

@@ -37,12 +37,79 @@ if ( $is_edit_mode ) {
$form_id = DDHH_JM_Formidable::get_job_edit_form_id();
if ( $form_id ) {
// Get post data
$post = get_post( $job_id );
if ( ! $post || 'job_offer' !== $post->post_type || absint( $post->post_author ) !== get_current_user_id() ) {
echo '<div class="ddhh-error-message"><p>Sie haben keine Berechtigung, dieses Stellenangebot zu bearbeiten.</p></div>';
return;
}
// Get field IDs
$fields = FrmField::getAll( array( 'fi.form_id' => $form_id ), 'field_order' );
$field_params = array();
foreach ( $fields as $field ) {
$field_value = '';
switch ( $field->field_key ) {
case 'job_title2':
$field_value = $post->post_title;
break;
case 'job_description2':
$field_value = $post->post_content;
break;
case 'job_location2':
$field_value = get_post_meta( $job_id, 'job_location', true );
break;
case 'job_type2':
$field_value = get_post_meta( $job_id, 'job_type', true );
break;
case 'job_deadline2':
$field_value = get_post_meta( $job_id, 'job_deadline', true );
break;
case 'job_contact_email2':
$field_value = get_post_meta( $job_id, 'job_contact_email', true );
break;
case 'job_logo2':
$field_value = get_post_thumbnail_id( $job_id );
break;
}
if ( ! empty( $field_value ) ) {
$field_params[ $field->id ] = $field_value;
}
}
?>
<div class="ddhh-provider-dashboard">
<div class="ddhh-job-edit-section">
<h2>Stellenangebot bearbeiten</h2>
<p><a href="<?php echo esc_url( get_permalink() ); ?>" class="back-to-dashboard">← Zurück zur Übersicht</a></p>
<?php echo do_shortcode( "[formidable id={$form_id} id_param={$job_id}]" ); ?>
<p><a href="<?php echo esc_url( home_url( '/anbieter-dashboard/' ) ); ?>" class="back-to-dashboard">← Zurück zur Übersicht</a></p>
<?php echo do_shortcode( "[formidable id={$form_id}]" ); ?>
<script type="text/javascript">
jQuery(document).ready(function($) {
// Populate form fields with existing data
var formData = <?php echo json_encode( $field_params ); ?>;
// Wait for form to be fully loaded
setTimeout(function() {
$.each(formData, function(fieldId, value) {
var field = $('input[name="item_meta[' + fieldId + ']"], textarea[name="item_meta[' + fieldId + ']"], select[name="item_meta[' + fieldId + ']"]');
if (field.length) {
if (field.is('select')) {
field.val(value).trigger('change');
} else if (field.is(':checkbox') || field.is(':radio')) {
field.filter('[value="' + value + '"]').prop('checked', true);
} else {
field.val(value);
}
console.log('Populated field ' + fieldId + ' with:', value);
}
});
}, 500);
});
</script>
</div>
</div>
<?php
@@ -59,7 +126,7 @@ if ( $is_deactivate_mode ) {
<div class="ddhh-provider-dashboard">
<div class="ddhh-job-deactivate-section">
<h2>Stellenangebot deaktivieren</h2>
<p><a href="<?php echo esc_url( get_permalink() ); ?>" class="back-to-dashboard">← Zurück zur Übersicht</a></p>
<p><a href="<?php echo esc_url( home_url( '/anbieter-dashboard/' ) ); ?>" class="back-to-dashboard">← Zurück zur Übersicht</a></p>
<?php echo do_shortcode( "[formidable id={$form_id} id_param={$job_id}]" ); ?>
</div>
</div>
@@ -137,12 +204,14 @@ $job_query = new WP_Query( $args );
<?php
// Edit link - points to edit form on dashboard
if ( current_user_can( 'edit_job_offer', $post_id ) ) {
$dashboard_url = home_url( '/anbieter-dashboard/' );
$edit_url = add_query_arg(
array(
'action' => 'edit_job',
'job_id' => $post_id,
'action' => 'edit_job',
'job_id' => $post_id,
'id_param' => $post_id, // For Formidable form action
),
get_permalink()
$dashboard_url
);
echo '<a href="' . esc_url( $edit_url ) . '" class="button edit-link">Bearbeiten</a>';
}
@@ -154,12 +223,13 @@ $job_query = new WP_Query( $args );
// Deactivate link - only for published posts
if ( 'publish' === $post_status ) {
$dashboard_url = home_url( '/anbieter-dashboard/' );
$deactivate_url = add_query_arg(
array(
'action' => 'deactivate_job',
'job_id' => $post_id,
),
get_permalink()
$dashboard_url
);
echo ' <a href="' . esc_url( $deactivate_url ) . '" class="button deactivate-link">Deaktivieren</a>';
}