Compare commits

...

10 Commits

Author SHA1 Message Date
369870ef00 docs: add comprehensive CLAUDE.md for future AI assistance
Add detailed documentation covering plugin architecture, subsystems, workflows, and development practices. Includes GSD workflow requirement for all code changes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 23:10:44 +09:00
1b41b72a3d feat(contact-form): implement modal contact form with AJAX submission
Replace mailto link with modal popup containing Formidable job application form. Modal stays open after submission to show success message.

Changes:
- Add modal popup with contact form on job detail pages
- Implement AJAX form submission to prevent page reload
- Auto-populate job_id field when modal opens
- Add field key compatibility for both job_id and job_id2
- Fix form ID comparison to use loose equality
- Keep modal open after submission to display success message
- Add modal styling and close functionality

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 22:56:57 +09:00
907b5a9924 fix(07-01): convert dates back to ISO format before form submission
Added form submit handler that converts DD.MM.YYYY dates back to YYYY-MM-DD format before submission. This fixes validation error "Bewerbungsfrist is invalid" by ensuring Formidable Forms receives dates in the expected format.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:59:39 +09:00
a87b48df68 fix(07-01): auto-convert date slashes to dots after picker selection
Added change event listener that automatically replaces forward slashes with dots in date fields after user makes a selection. This ensures consistent DD.MM.YYYY format throughout the form interaction.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:57:57 +09:00
f229af23f5 feat(07-01): improve date field prepopulation with format conversion
Added JavaScript to convert YYYY-MM-DD dates to DD.MM.YYYY format when prepopulating date fields. This is a partial fix for the date field issues - full fix requires Formidable Forms configuration.

Also updated ISSUES.md with detailed documentation of all three date field problems: initial format, picker display, and post-selection format.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:43:42 +09:00
18ddcd5e0a feat(07-01): add deadline and contact email to all notification emails
All three job notification emails (new submission, edit, deactivation) now include:
- Bewerbungsfrist (deadline) - formatted as DD.MM.YYYY
- Kontakt-E-Mail (contact email)

This provides administrators with complete information about each job posting.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:36:43 +09:00
0c9ebb9e89 fix(07-01): fix job type dropdown not pre-populating on edit
Improved JavaScript to use case-insensitive matching for select fields, so 'vollzeit' matches 'Vollzeit'. Also normalized all job_type values to lowercase when saving to ensure consistency.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:27:49 +09:00
9142b56a9f docs(07-01): add issue for date field display format
Date field shows as '20260130' instead of German format '30.01.2026'. Documented as low priority cosmetic issue to be fixed later.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:23:59 +09:00
32a22e5fd6 fix(07-01): allow providers to edit published job offers
Added edit_published_job_offers capability to ddhh_provider role and created upgrade_roles function to automatically add this capability to existing provider accounts. Providers can now edit their published job offers from the dashboard.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 21:20:16 +09:00
7f2c5fa6a6 feat(07-01): add frontend display template for job offers
Created DDHH_JM_Template class to display full job details on single job offer pages. Shows logo, organization, location, type, deadline, description, and contact information in a styled layout.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 20:13:48 +09:00
9 changed files with 815 additions and 10 deletions

View File

@@ -27,6 +27,41 @@
5. Modify job detail template to display provider's company logo instead
6. Add logo display to provider dashboard (show their uploaded logo)
## Date Field Format and Pre-population Issues (Discovered during 07-01 testing)
**Issue:** The deadline date field has multiple formatting and pre-population problems
**Current Behavior:**
1. **Initial display**: Shows as "2026-01-31" (YYYY-MM-DD) instead of German format
2. **Date picker bug**: Shows wrong date (current date instead of saved value)
- Saved: 2026-01-31
- Picker shows: 2026-01-17 (today)
3. **Post-selection format**: Changes to "13/02/2026" (DD/MM/YYYY with slashes)
**Expected Behavior:**
- Date should always display as "31.01.2026" (DD.MM.YYYY with dots)
- Date picker should show the saved date, not current date
- Format should remain consistent before and after selection
**Location:**
- Provider Dashboard edit form (`/anbieter-dashboard/?action=edit_job&job_id=XXX`)
- Specifically the "Bewerbungsfrist" (deadline) field
**Impact:** Medium - Confusing UX, users might select wrong dates
**Priority:** Medium
**Phase Discovered:** 07-01 (Provider flow testing)
**Root Cause:**
This is a Formidable Forms date field configuration issue, not a plugin code issue.
**Fix Required:**
1. Access Formidable Forms field settings for "job_deadline2" field
2. Set date format to: `d.m.Y` (PHP date format for DD.MM.YYYY)
3. Configure datepicker options to use dots instead of slashes
4. Ensure the JavaScript prepopulation correctly parses the saved YYYY-MM-DD value
5. May need custom JavaScript to convert saved value to picker-friendly format
6. Test thoroughly: initial load, picker display, post-selection format
## Empty "Art der Stelle" Dropdown (Fixed during 07-01 testing)
**Issue:** The "Art der Stelle" dropdown was empty on job submission form

277
CLAUDE.md Normal file
View File

@@ -0,0 +1,277 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Digital Dabei Job Manager is a WordPress plugin providing a closed job board for "digital dabei Hamburg". External organizations (providers) self-register and manage job listings. Mentors (existing subscribers) view and apply to jobs. All jobs require admin moderation before publication.
**Core principle**: Every job goes through admin approval before mentors see it. The moderation flow is the trust layer protecting mentors from spam.
## Development Environment
- **Platform**: Local WP (WordPress development environment)
- **WordPress**: 6.0+, PHP 7.4+
- **Required Plugins**: ACF Pro, Formidable Forms Pro, Elementor Pro
- **Email**: WP Mail SMTP on production; disabled in Local WP dev environment
## Development Workflow with GSD
**IMPORTANT**: Use `/gsd` commands for all code-related work in this repository.
The GSD (Get Stuff Done) workflow provides:
- Structured planning and execution phases
- Automatic verification of changes
- State management across sessions
- Parallel execution when possible
Common commands:
- `/gsd:progress` - Check current state and next steps
- `/gsd:plan-phase` - Plan implementation for a phase
- `/gsd:execute-phase` - Execute all plans in a phase
- `/gsd:help` - View all available GSD commands
## Architecture
### Plugin Entry Point
The plugin follows WordPress singleton pattern with class-based initialization:
1. `ddhh-job-manager.php` - Main plugin file that:
- Loads Action Scheduler vendor library **first** (required for async operations)
- Defines constants (DDHH_JM_VERSION, DDHH_JM_PLUGIN_DIR, etc.)
- Requires all class files from `includes/`
- Initializes via `DDHH_JM_Job_Manager::get_instance()` on `plugins_loaded` hook
2. `includes/class-ddhh-job-manager.php` - Singleton orchestrator that initializes all subsystems via `init_hooks()`
### Core Subsystems
The plugin is organized into focused class files in `includes/`:
**Foundation Layer**:
- `class-post-types.php` - Registers `job_offer` CPT with custom capabilities
- `class-roles.php` - Manages `ddhh_provider` role with restricted capabilities
- `class-acf-fields.php` - Programmatically registers ACF field groups for job metadata
- `class-activator.php` / `class-deactivator.php` - Plugin lifecycle hooks
**User Management**:
- `class-formidable.php` - Integration layer for all Formidable Forms (registration, job submission, edit, deactivation, application). Uses form keys like 'provider_registration', 'job_submission', etc.
- `class-access-control.php` - Blocks providers from WP-Admin, enforces login requirements on job archives
- `class-user-preferences.php` - Manages mentor opt-in preferences for job notifications
**Job Workflows**:
- `class-dashboard.php` - Provides `[ddhh_provider_dashboard]` shortcode displaying provider's jobs
- `class-pages.php` - Creates/manages plugin pages (dashboard, login) via option storage
- `class-archive.php` - Filters job queries to show only published jobs to appropriate users
- `class-template.php` - Handles single job display and contact form modal
**Notifications & Async Processing**:
- `class-notifications.php` - Email system triggered by WordPress hooks (`ddhh_job_submitted`, `transition_post_status`, `frm_after_create_entry`)
- `class-scheduler.php` - Action Scheduler wrapper for async batch email processing (chunks of 50 users)
**Admin Experience**:
- `class-admin-ui.php` - Enhances admin screens with custom columns, quick links, status indicators
### Custom Roles & Capabilities
**ddhh_provider role**: Restricted role for external organizations
- Can create/edit/delete only their own `job_offer` posts
- Cannot access WP-Admin (redirected to frontend dashboard)
- Can edit their own profile (`profile.php` access allowed)
**Capability mapping** (in `class-post-types.php`):
- `edit_job_offer` - Owner can edit their own jobs
- `delete_job_offer` - Owner can delete their own jobs
- `read_job_offer` - Users can read published jobs
- Standard WordPress `map_meta_cap` filter enforces ownership
### Formidable Forms Integration
The plugin uses 5 Formidable forms, identified by form keys:
1. **provider_registration** (F1) - Creates user with `ddhh_provider` role, auto-login
2. **job_submission** (F2) - Creates `job_offer` post with `pending` status, maps ACF fields
3. **job_edit** (F3) - Updates existing job (ownership validated), preserves submission date
4. **job_deactivation** (F4) - Sets status to `draft`, captures deactivation reason
5. **job_application** (F5) - Mentor applies to job, emails provider contact address
**Form hooks** (`class-formidable.php`):
- Registration: `frm_after_create_entry` → auto-login → redirect to dashboard
- Job submission: `frm_after_create_entry` → fires `ddhh_job_submitted` action → admin notification
- Job edit: `frm_after_update_entry` → fires `ddhh_job_edited` action → admin notification
- Application: `frm_after_create_entry` → emails provider's contact email from ACF field
### Email Notification System
**Immediate notifications** (`class-notifications.php`):
- Admin receives email on job submission with edit/view links
- Admin receives email on job edit with change summary
- Admin receives email on deactivation with reason
- Provider receives email when mentor applies (via Formidable)
**Async batch notifications** (`class-scheduler.php`):
- Mentor notification on job publish uses Action Scheduler
- Users are queried for `ddhh_jm_notification_optin` meta
- Batched into groups of 50 to prevent email provider limits
- Each batch scheduled as separate `ddhh_jm_send_mentor_notification_batch` action
### Access Control Layers
1. **WP-Admin lockout**: Providers redirected to dashboard unless accessing `profile.php` or AJAX
2. **Dashboard protection**: `template_redirect` hook checks user role, redirects non-providers
3. **Archive protection**: Jobs require login; only `publish` status shown to mentors
4. **Single job protection**: Login required to view job details
5. **Ownership validation**: Forms validate current user owns the job before editing/deactivation
### Template System
**Frontend templates** (`templates/`):
- `provider-dashboard.php` - Displays provider's jobs in table format with status badges and action links
**Elementor integration**:
- Job archive uses Elementor Loop Grid (no custom template file)
- Job detail page uses Elementor single post template with ACF dynamic tags
- Contact form modal injected via `the_content` filter in `class-template.php`
### Job Lifecycle
```
Provider submits → pending → Admin reviews → publish → Mentors see + notified
Admin can reject → trash
Provider deactivates → draft (with reason stored in meta)
```
**Status hooks**:
- `ddhh_job_submitted` - Fired after new job metadata saved (form entry ID passed)
- `ddhh_job_edited` - Fired after job update metadata saved (form entry ID passed)
- `transition_post_status` - Used for publish detection and deactivation detection
### ACF Field Groups
Programmatically registered in `class-acf-fields.php`:
- **Job Details**: `job_location`, `job_type`, `job_deadline` (date picker), `contact_email`
- **Deactivation**: `deactivation_reason`, `deactivation_date`
- **Metadata**: `submission_date` (preserved during edits)
All fields support German labels matching site language.
## Development Workflow
### Testing Locally
Since email is disabled in Local WP, test email functionality by:
1. Checking Action Scheduler status: WP Admin → Tools → Scheduled Actions
2. Monitoring logs: `error_log()` calls throughout notification classes
3. Using a plugin like WP Mail Logging to capture emails during dev
### Working with Forms
Formidable forms are managed through the Formidable UI. When referencing forms in code:
- Use form keys (e.g., 'job_submission') not IDs
- Form ID lookup happens via `FrmForm::getOne('form_key')`
- Field IDs are stored in form configuration, not hardcoded
### Modifying Email Templates
Email templates are inline in `class-notifications.php`:
- Admin emails use `wp_mail()` with HTML content
- Mentor notification emails use `wp_mail()` with plain text
- All emails include job title, provider name, and relevant links
- Date formatting: Use `get_field()` for ACF dates, format with German locale
### Adding New Notifications
1. Identify triggering event (form submission, status change, custom action)
2. Add notification method to `class-notifications.php`
3. Hook into appropriate action in `setup_hooks()`
4. For batch processing, use `DDHH_JM_Scheduler::schedule_mentor_notification_batch()`
## Common Tasks
### Debugging Action Scheduler
```bash
# View scheduled actions in WP CLI
wp action-scheduler list --status=pending
wp action-scheduler list --status=failed
# Run pending actions manually
wp action-scheduler run --batch-size=10
# Clean up old actions
wp action-scheduler clean --days=90
```
### Checking Provider Permissions
Providers should NOT be able to:
- Access WP-Admin (except profile.php)
- Edit other providers' jobs
- Publish jobs directly (must be pending)
- View unpublished jobs by other providers
### Verifying Job Moderation Flow
1. Log in as provider → Submit job → Should create `pending` post
2. Admin receives email with job details and moderation links
3. Admin publishes job → Opted-in mentors receive notification (async batches)
4. Provider sees published job in dashboard
5. Mentors can view and apply to job
### Date Field Handling
Job deadlines use ACF date picker:
- Format conversion in Formidable forms: Slashes auto-converted to dots for display
- Before submission: Dots converted back to ISO format (YYYY-MM-DD)
- Display: Use `get_field('job_deadline')` returns 'Ymd', format with `date_i18n()`
- Validation: Date fields are optional (empty string is valid)
### Logo Auto-Cropping
Featured images (logos) auto-crop to 200×200px on upload:
- Handled by `class-post-types.php` registering 'job-logo' image size
- Uses WordPress `add_image_size()` with hard crop enabled
- Images uploaded via Formidable forms trigger standard WP media processing
## Project State
**Currently**: Phase 6 (Email Notifications) is mostly complete. Phase 7 (Testing & Polish) is next.
**What's working**:
- Provider registration and authentication
- Job submission, editing, deactivation workflows
- Admin moderation and notifications
- Mentor job browsing and applications
- Opt-in preferences for mentor notifications
- Action Scheduler integration for async processing
**What's pending**:
- Phase 6.3: Async batch email processing on job publish (implementation in progress)
- Phase 7: End-to-end testing and production deployment prep
**Documentation**: See `.planning/` for detailed project documentation:
- `PROJECT.md` - Requirements and context
- `ROADMAP.md` - Phase breakdown and progress
- `STATE.md` - Current work and blockers
- `phases/` - Detailed plans and summaries for each phase
## Code Standards
- **WordPress Coding Standards**: Follow WordPress PHP coding standards
- **Text Domain**: Use `ddhh-job-manager` for all translatable strings
- **German Language**: All user-facing text in German
- **Security**: Validate ownership, escape output, verify nonces in forms
- **Hook Naming**: Use `ddhh_jm_` prefix for custom actions/filters
- **Class Naming**: Use `DDHH_JM_` prefix, one class per file
- **Static Methods**: Use static methods for classes that don't maintain state
- **Direct Exit**: Always include `defined( 'ABSPATH' ) || exit;` at top of PHP files
## Critical Files to Review Before Major Changes
- `ddhh-job-manager.php` - Plugin initialization order matters (Action Scheduler first)
- `class-ddhh-job-manager.php` - Hook registration sequence
- `class-formidable.php` - Form IDs and field mappings
- `class-notifications.php` - Email templates and triggering logic
- `class-access-control.php` - Security boundaries for providers
- `class-post-types.php` - Capability mapping for `job_offer` posts

View File

@@ -41,6 +41,7 @@ require_once DDHH_JM_PLUGIN_DIR . 'includes/class-dashboard.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-access-control.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-notifications.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-archive.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-template.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-admin-ui.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-user-preferences.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-scheduler.php';

View File

@@ -50,6 +50,9 @@ class DDHH_JM_Job_Manager {
// Initialize post types
add_action( 'init', array( 'DDHH_JM_Post_Types', 'register' ) );
// Upgrade roles with any new capabilities
add_action( 'init', array( 'DDHH_JM_Roles', 'upgrade_roles' ) );
// Initialize ACF fields
add_action( 'acf/init', array( 'DDHH_JM_ACF_Fields', 'register_fields' ) );
@@ -68,6 +71,9 @@ class DDHH_JM_Job_Manager {
// Initialize archive query helper
add_action( 'init', array( 'DDHH_JM_Archive', 'setup_hooks' ) );
// Initialize template display
add_action( 'init', array( 'DDHH_JM_Template', 'setup_hooks' ) );
// Initialize admin UI enhancements (admin-only)
if ( is_admin() ) {
add_action( 'init', array( 'DDHH_JM_Admin_UI', 'setup_hooks' ) );

View File

@@ -174,6 +174,9 @@ class DDHH_JM_Formidable {
// Hook to pre-populate edit form fields
add_filter( 'frm_get_default_value', array( __CLASS__, 'prepopulate_edit_form_fields' ), 10, 3 );
// Hook to pre-populate job_id in application form
add_filter( 'frm_get_default_value', array( __CLASS__, 'prepopulate_application_job_id' ), 10, 3 );
}
/**
@@ -619,7 +622,7 @@ class DDHH_JM_Formidable {
break;
case 'job_type':
case 'job_type2':
$job_type = sanitize_text_field( $value );
$job_type = strtolower( sanitize_text_field( $value ) );
break;
case 'job_deadline':
case 'job_deadline2':
@@ -1425,4 +1428,46 @@ class DDHH_JM_Formidable {
FrmField::create( $field );
}
}
/**
* Pre-populate job_id field in application form
*
* @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_application_job_id( $default_value, $field, $dynamic_default ) {
// Only process for the job application form
if ( absint( $field->form_id ) !== self::get_job_application_form_id() ) {
return $default_value;
}
// Only process the job_id field
if ( 'job_id' !== $field->field_key ) {
return $default_value;
}
// Check for job_id in shortcode attributes (passed from template)
// Formidable stores shortcode attributes in a global variable
global $frm_vars;
if ( isset( $frm_vars['job_id'] ) ) {
return absint( $frm_vars['job_id'] );
}
// Fallback: Check URL parameter
if ( isset( $_GET['job_id'] ) ) {
return absint( $_GET['job_id'] );
}
// Fallback: Try to get from current post if we're on a single job page
if ( is_singular( 'job_offer' ) ) {
global $post;
if ( $post && 'job_offer' === $post->post_type ) {
return absint( $post->ID );
}
}
return $default_value;
}
}

View File

@@ -62,8 +62,16 @@ class DDHH_JM_Notifications {
}
// Get post meta fields
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
// Format deadline if present
$deadline_formatted = 'Nicht angegeben';
if ( ! empty( $job_deadline ) ) {
$deadline_formatted = date( 'd.m.Y', strtotime( $job_deadline ) );
}
// Get submission date
$submission_date = get_the_date( 'd.m.Y H:i', $post->ID );
@@ -81,6 +89,8 @@ class DDHH_JM_Notifications {
"Anbieter: %s (%s)\n" .
"Standort: %s\n" .
"Art: %s\n" .
"Bewerbungsfrist: %s\n" .
"Kontakt-E-Mail: %s\n" .
"Eingereicht am: %s\n\n" .
"Prüfen Sie das Stellenangebot hier:\n%s\n\n" .
"---\n" .
@@ -90,6 +100,8 @@ class DDHH_JM_Notifications {
$author_org,
$job_location ? $job_location : 'Nicht angegeben',
$job_type ? $job_type : 'Nicht angegeben',
$deadline_formatted,
$job_contact_email ? $job_contact_email : 'Nicht angegeben',
$submission_date,
$edit_link
);
@@ -144,8 +156,16 @@ class DDHH_JM_Notifications {
}
// Get post meta fields
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
// Format deadline if present
$deadline_formatted = 'Nicht angegeben';
if ( ! empty( $job_deadline ) ) {
$deadline_formatted = date( 'd.m.Y', strtotime( $job_deadline ) );
}
// Get submission date
$submission_date = current_time( 'd.m.Y H:i' );
@@ -163,6 +183,8 @@ class DDHH_JM_Notifications {
"Anbieter: %s (%s)\n" .
"Standort: %s\n" .
"Art: %s\n" .
"Bewerbungsfrist: %s\n" .
"Kontakt-E-Mail: %s\n" .
"Bearbeitet am: %s\n\n" .
"Prüfen Sie das Stellenangebot hier:\n%s\n\n" .
"---\n" .
@@ -172,6 +194,8 @@ class DDHH_JM_Notifications {
$author_org,
$job_location ? $job_location : 'Nicht angegeben',
$job_type ? $job_type : 'Nicht angegeben',
$deadline_formatted,
$job_contact_email ? $job_contact_email : 'Nicht angegeben',
$submission_date,
$edit_link
);
@@ -233,8 +257,16 @@ class DDHH_JM_Notifications {
}
// Get post meta fields
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
// Format deadline if present
$deadline_formatted = 'Nicht angegeben';
if ( ! empty( $job_deadline ) ) {
$deadline_formatted = date( 'd.m.Y', strtotime( $job_deadline ) );
}
// Get deactivation reason from post meta
$deactivation_reason = get_post_meta( $post->ID, 'job_deactivation_reason', true );
@@ -258,6 +290,8 @@ class DDHH_JM_Notifications {
"Anbieter: %s (%s)\n" .
"Standort: %s\n" .
"Art: %s\n" .
"Bewerbungsfrist: %s\n" .
"Kontakt-E-Mail: %s\n" .
"Deaktiviert am: %s\n\n" .
"Grund für Deaktivierung:\n%s\n\n" .
"Stelle ansehen:\n%s\n\n" .
@@ -268,6 +302,8 @@ class DDHH_JM_Notifications {
$author_org,
$job_location ? $job_location : 'Nicht angegeben',
$job_type ? $job_type : 'Nicht angegeben',
$deadline_formatted,
$job_contact_email ? $job_contact_email : 'Nicht angegeben',
$deactivation_date,
$deactivation_reason ? $deactivation_reason : 'Nicht angegeben',
$edit_link
@@ -303,7 +339,7 @@ class DDHH_JM_Notifications {
public static function send_provider_application_notification( $entry_id, $form_id ) {
// Only process job application form submissions
$application_form_id = DDHH_JM_Formidable::get_job_application_form_id();
if ( $form_id !== $application_form_id ) {
if ( $form_id != $application_form_id ) {
return;
}
@@ -336,6 +372,7 @@ class DDHH_JM_Notifications {
$applicant_message = sanitize_textarea_field( $value );
break;
case 'job_id':
case 'job_id2':
$job_id = absint( $value );
break;
}

View File

@@ -28,6 +28,7 @@ class DDHH_JM_Roles {
// Job offer capabilities (own only)
'edit_job_offers' => true,
'edit_published_job_offers' => true,
'delete_job_offers' => true,
'upload_files' => true,
@@ -70,11 +71,28 @@ class DDHH_JM_Roles {
}
}
/**
* Upgrade existing roles with new capabilities
* Called on plugin init to ensure all capabilities are present
*/
public static function upgrade_roles() {
$provider_role = get_role( 'ddhh_provider' );
if ( $provider_role && ! $provider_role->has_cap( 'edit_published_job_offers' ) ) {
$provider_role->add_cap( 'edit_published_job_offers' );
}
}
/**
* Remove custom roles
* Called on plugin deactivation
*/
public static function remove_roles() {
// Remove provider role capabilities before removing role
$provider_role = get_role( 'ddhh_provider' );
if ( $provider_role ) {
$provider_role->remove_cap( 'edit_published_job_offers' );
}
remove_role( 'ddhh_provider' );
// Remove job_offer capabilities from administrator

325
includes/class-template.php Normal file
View File

@@ -0,0 +1,325 @@
<?php
/**
* Template functionality for job offers
*
* @package DDHH_Job_Manager
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Handles template display for job offers
*/
class DDHH_JM_Template {
/**
* Setup template hooks
*/
public static function setup_hooks() {
// Filter the content to add job details
add_filter( 'the_content', array( __CLASS__, 'add_job_details_to_content' ) );
}
/**
* Add job details to the content for single job_offer posts
*
* @param string $content Post content.
* @return string Modified content.
*/
public static function add_job_details_to_content( $content ) {
// Only modify content on single job_offer posts
if ( ! is_singular( 'job_offer' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
global $post;
// Get job meta data
$job_location = get_post_meta( $post->ID, 'job_location', true );
$job_type = get_post_meta( $post->ID, 'job_type', true );
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
$job_logo = get_the_post_thumbnail( $post->ID, 'job-logo' );
// Get author/organization info
$author = get_userdata( $post->post_author );
$author_name = $author ? $author->display_name : '';
$author_org = get_user_meta( $post->post_author, 'ddhh_org_name', true );
// Build job details HTML
ob_start();
?>
<div class="ddhh-job-offer-details">
<?php if ( $job_logo ) : ?>
<div class="job-logo">
<?php echo $job_logo; ?>
</div>
<?php endif; ?>
<div class="job-meta">
<?php if ( $author_org ) : ?>
<div class="job-meta-item">
<strong>Anbieter:</strong> <?php echo esc_html( $author_org ); ?>
</div>
<?php endif; ?>
<?php if ( $job_location ) : ?>
<div class="job-meta-item">
<strong>Standort:</strong> <?php echo esc_html( $job_location ); ?>
</div>
<?php endif; ?>
<?php if ( $job_type ) : ?>
<div class="job-meta-item">
<strong>Art:</strong> <?php echo esc_html( $job_type ); ?>
</div>
<?php endif; ?>
<?php if ( $job_deadline ) : ?>
<div class="job-meta-item">
<strong>Bewerbungsfrist:</strong> <?php echo esc_html( date( 'd.m.Y', strtotime( $job_deadline ) ) ); ?>
</div>
<?php endif; ?>
</div>
<div class="job-description">
<h3>Stellenbeschreibung</h3>
<?php echo wp_kses_post( $content ); ?>
</div>
<?php if ( $job_contact_email && is_user_logged_in() && current_user_can( 'read' ) ) : ?>
<div class="job-application-section">
<h3>Interesse?</h3>
<button type="button" class="ddhh-contact-button" onclick="ddhhOpenContactModal(<?php echo absint( $post->ID ); ?>)">
Jetzt Kontakt aufnehmen
</button>
</div>
<!-- Contact Form Modal -->
<div id="ddhh-contact-modal-<?php echo absint( $post->ID ); ?>" class="ddhh-modal" style="display: none;">
<div class="ddhh-modal-content">
<span class="ddhh-modal-close" onclick="ddhhCloseContactModal(<?php echo absint( $post->ID ); ?>)">&times;</span>
<h3>Kontakt aufnehmen</h3>
<div class="ddhh-modal-body">
<?php
// Get the job application form ID
$form_id = DDHH_JM_Formidable::get_job_application_form_id();
if ( $form_id ) {
// Render the Formidable form with AJAX enabled to prevent page reload
echo do_shortcode( '[formidable id="' . absint( $form_id ) . '" job_id="' . absint( $post->ID ) . '" ajax="true"]' );
} else {
echo '<p>Formular konnte nicht geladen werden. Bitte kontaktieren Sie uns unter: <a href="mailto:' . esc_attr( $job_contact_email ) . '">' . esc_html( $job_contact_email ) . '</a></p>';
}
?>
</div>
</div>
</div>
<?php endif; ?>
<style>
.ddhh-job-offer-details {
margin: 2em 0;
}
.ddhh-job-offer-details .job-logo {
margin-bottom: 2em;
}
.ddhh-job-offer-details .job-logo img {
max-width: 200px;
height: auto;
}
.ddhh-job-offer-details .job-meta {
background: #f5f5f5;
padding: 1.5em;
margin-bottom: 2em;
border-radius: 4px;
}
.ddhh-job-offer-details .job-meta-item {
margin-bottom: 0.75em;
}
.ddhh-job-offer-details .job-meta-item:last-child {
margin-bottom: 0;
}
.ddhh-job-offer-details .job-meta-item strong {
display: inline-block;
min-width: 150px;
}
.ddhh-job-offer-details .job-description {
margin-bottom: 2em;
}
.ddhh-job-offer-details .job-application-section {
background: #e8f4f8;
padding: 1.5em;
border-left: 4px solid #0073aa;
border-radius: 4px;
text-align: center;
}
.ddhh-job-offer-details .job-application-section h3 {
margin-top: 0;
}
.ddhh-job-offer-details .ddhh-contact-button {
background: #0073aa;
color: white;
border: none;
padding: 12px 30px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s ease;
}
.ddhh-job-offer-details .ddhh-contact-button:hover {
background: #005a87;
}
/* Modal Styles */
.ddhh-modal {
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.6);
}
.ddhh-modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 30px;
border: 1px solid #888;
border-radius: 8px;
width: 90%;
max-width: 600px;
position: relative;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.ddhh-modal-close {
color: #aaa;
float: right;
font-size: 32px;
font-weight: bold;
line-height: 20px;
cursor: pointer;
transition: color 0.3s ease;
}
.ddhh-modal-close:hover,
.ddhh-modal-close:focus {
color: #000;
}
.ddhh-modal-body {
margin-top: 20px;
}
.ddhh-modal h3 {
margin-top: 0;
color: #333;
}
</style>
<script>
function ddhhOpenContactModal(jobId) {
var modal = document.getElementById('ddhh-contact-modal-' + jobId);
if (modal) {
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
// Set the job_id in the hidden field
var jobIdField = modal.querySelector('input[id*="job_id"]');
if (jobIdField) {
jobIdField.value = jobId;
}
// Prevent form from redirecting on submit
var form = modal.querySelector('form.frm-show-form');
if (form && !form.dataset.ddhhHandled) {
form.dataset.ddhhHandled = 'true';
jQuery(form).on('submit', function(e) {
e.preventDefault();
console.log('Form submit intercepted - preventing page reload');
var $form = jQuery(this);
var formData = new FormData(this);
// Submit form via AJAX
jQuery.ajax({
url: $form.attr('action') || window.location.href,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log('Form submitted successfully via AJAX');
// Replace form content with response
var formContainer = modal.querySelector('.ddhh-modal-body');
if (formContainer) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = response;
var newForm = tempDiv.querySelector('form.frm-show-form');
if (newForm) {
formContainer.innerHTML = newForm.outerHTML;
} else {
// Show success message if no form in response
var successMsg = tempDiv.querySelector('.frm_message');
if (successMsg) {
formContainer.innerHTML = successMsg.outerHTML;
}
}
}
},
error: function() {
console.error('Form submission failed');
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
});
return false;
});
}
}
}
function ddhhCloseContactModal(jobId) {
var modal = document.getElementById('ddhh-contact-modal-' + jobId);
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
// Close modal when clicking outside of it
window.onclick = function(event) {
if (event.target.classList.contains('ddhh-modal')) {
event.target.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
// Handle successful form submission - keep modal open to show success message
jQuery(document).ready(function($) {
// Prevent redirect after form submission
$(document).on('frmBeforeFormRedirect', function(event, form, response) {
var formKey = $(form).find('input[name="form_key"]').val();
if (formKey === 'job_application') {
// Prevent the default redirect behavior
event.preventDefault();
return false;
}
});
$(document).on('frmFormComplete', function(event, form, response) {
// Check if this is the job application form
var formKey = $(form).find('input[name="form_key"]').val();
if (formKey === 'job_application') {
// Modal stays open so user can see success message
// User can close it manually by clicking X or outside the modal
console.log('Form submitted successfully, modal staying open');
}
});
});
</script>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -98,16 +98,77 @@ if ( $is_edit_mode ) {
if (field.length) {
if (field.is('select')) {
field.val(value).trigger('change');
// For select fields, try case-insensitive matching
var options = field.find('option');
var matched = false;
options.each(function() {
if ($(this).val().toLowerCase() === value.toLowerCase()) {
field.val($(this).val()).trigger('change');
matched = true;
console.log('Matched select option:', $(this).val(), 'for value:', value);
return false; // break
}
});
if (!matched) {
console.warn('No matching option found for field', fieldId, 'value:', value, 'Available options:', options.map(function() { return $(this).val(); }).get());
}
} else if (field.is(':checkbox') || field.is(':radio')) {
field.filter('[value="' + value + '"]').prop('checked', true);
} else {
field.val(value);
// For date fields, convert YYYY-MM-DD to DD.MM.YYYY format
if (field.attr('type') === 'date' || field.hasClass('frm_date')) {
// Check if value is in YYYY-MM-DD format
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
var parts = value.split('-');
var formattedDate = parts[2] + '.' + parts[1] + '.' + parts[0];
field.val(formattedDate).trigger('change');
console.log('Converted date from', value, 'to', formattedDate);
} else {
field.val(value);
}
} else {
field.val(value);
}
}
console.log('Populated field ' + fieldId + ' with:', value);
} else {
console.warn('Field not found:', fieldId);
}
});
}, 500);
// Fix date format after user selects a date - replace slashes with dots for display
$(document).on('change', 'input[type="date"], input.frm_date', function() {
var field = $(this);
var value = field.val();
// If the value contains slashes, replace them with dots
if (value && value.includes('/')) {
var fixedValue = value.replace(/\//g, '.');
field.val(fixedValue);
// Store the original format in a data attribute for form submission
field.data('original-format', value.replace(/\//g, '-'));
console.log('Fixed date format from', value, 'to', fixedValue);
}
});
// Before form submission, convert dates back to YYYY-MM-DD format
$('form').on('submit', function() {
$(this).find('input[type="date"], input.frm_date').each(function() {
var field = $(this);
var value = field.val();
// Convert DD.MM.YYYY or DD/MM/YYYY back to YYYY-MM-DD
if (value && /^\d{2}[./]\d{2}[./]\d{4}$/.test(value)) {
var parts = value.split(/[./]/);
var isoDate = parts[2] + '-' + parts[1] + '-' + parts[0];
field.val(isoDate);
console.log('Converted date for submission from', value, 'to', isoDate);
}
});
});
});
</script>
</div>