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 5. Modify job detail template to display provider's company logo instead
6. Add logo display to provider dashboard (show their uploaded logo) 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) ## Empty "Art der Stelle" Dropdown (Fixed during 07-01 testing)
**Issue:** The "Art der Stelle" dropdown was empty on job submission form **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-access-control.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-notifications.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-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-admin-ui.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-user-preferences.php'; require_once DDHH_JM_PLUGIN_DIR . 'includes/class-user-preferences.php';
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-scheduler.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 // Initialize post types
add_action( 'init', array( 'DDHH_JM_Post_Types', 'register' ) ); 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 // Initialize ACF fields
add_action( 'acf/init', array( 'DDHH_JM_ACF_Fields', 'register_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 // Initialize archive query helper
add_action( 'init', array( 'DDHH_JM_Archive', 'setup_hooks' ) ); 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) // Initialize admin UI enhancements (admin-only)
if ( is_admin() ) { if ( is_admin() ) {
add_action( 'init', array( 'DDHH_JM_Admin_UI', 'setup_hooks' ) ); 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 // Hook to pre-populate edit form fields
add_filter( 'frm_get_default_value', array( __CLASS__, 'prepopulate_edit_form_fields' ), 10, 3 ); 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; break;
case 'job_type': case 'job_type':
case 'job_type2': case 'job_type2':
$job_type = sanitize_text_field( $value ); $job_type = strtolower( sanitize_text_field( $value ) );
break; break;
case 'job_deadline': case 'job_deadline':
case 'job_deadline2': case 'job_deadline2':
@@ -1425,4 +1428,46 @@ class DDHH_JM_Formidable {
FrmField::create( $field ); 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

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

View File

@@ -28,6 +28,7 @@ class DDHH_JM_Roles {
// Job offer capabilities (own only) // Job offer capabilities (own only)
'edit_job_offers' => true, 'edit_job_offers' => true,
'edit_published_job_offers' => true,
'delete_job_offers' => true, 'delete_job_offers' => true,
'upload_files' => 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 * Remove custom roles
* Called on plugin deactivation * Called on plugin deactivation
*/ */
public static function remove_roles() { 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_role( 'ddhh_provider' );
// Remove job_offer capabilities from administrator // 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.length) {
if (field.is('select')) { 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')) { } else if (field.is(':checkbox') || field.is(':radio')) {
field.filter('[value="' + value + '"]').prop('checked', true); field.filter('[value="' + value + '"]').prop('checked', true);
} else {
// 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 { } else {
field.val(value); field.val(value);
} }
} else {
field.val(value);
}
}
console.log('Populated field ' + fieldId + ' with:', value); console.log('Populated field ' + fieldId + ' with:', value);
} else {
console.warn('Field not found:', fieldId);
} }
}); });
}, 500); }, 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> </script>
</div> </div>