Compare commits
16 Commits
v1.0
...
b7c6bb79e7
| Author | SHA1 | Date | |
|---|---|---|---|
| b7c6bb79e7 | |||
| 346ef5097b | |||
| f0de02ca94 | |||
| 0bef634eb8 | |||
| f8ec35e72f | |||
| 290c4e427f | |||
| 1b4449ae12 | |||
| 4145a92ca7 | |||
| 08b9ad24a5 | |||
| d907878143 | |||
| 38535b5edc | |||
| 98487eb05b | |||
| 0327d82ccf | |||
| 6f5288a355 | |||
| 3dab3f9034 | |||
| 84a4ae7c1b |
@@ -12,7 +12,7 @@ See: .planning/PROJECT.md (updated 2026-01-14)
|
|||||||
Phase: 7 of 7 (Testing & Polish)
|
Phase: 7 of 7 (Testing & Polish)
|
||||||
Plan: 3 of 3 in current phase
|
Plan: 3 of 3 in current phase
|
||||||
Status: Phase complete - PROJECT COMPLETE!
|
Status: Phase complete - PROJECT COMPLETE!
|
||||||
Last activity: 2026-01-29 — Completed Plan 07-03 (Admin Flow & Deployment Prep)
|
Last activity: 2026-01-29 — Completed Quick Task 002: Fix duplicate mentor notifications on job republish
|
||||||
|
|
||||||
Progress: ████████████████████ 100%
|
Progress: ████████████████████ 100%
|
||||||
|
|
||||||
@@ -97,27 +97,33 @@ Recent decisions affecting current work:
|
|||||||
|
|
||||||
### Deferred Issues
|
### Deferred Issues
|
||||||
|
|
||||||
**Post-deployment UX/notification improvements (4 non-blocking issues):**
|
**None - all known issues resolved!**
|
||||||
|
|
||||||
1. No logout option on /anbieter-login/ page - Low priority UX improvement
|
The 4 UX/notification issues identified during Phase 7 testing were fixed in Quick Task 001:
|
||||||
2. Admin submission email missing job description - Medium priority notification enhancement
|
- ✅ Provider login redirect (logged-in providers auto-redirect to dashboard)
|
||||||
3. Deactivation reason not showing in admin email - Medium priority notification fix
|
- ✅ Admin submission email includes job description
|
||||||
4. Admin email edit links not clickable - Medium priority email formatting fix
|
- ✅ Deactivation reason appears in admin email (timing bug fixed)
|
||||||
|
- ✅ Admin email edit links are clickable HTML hyperlinks
|
||||||
All issues documented in Phase 7 summaries. Core functionality is production-ready.
|
|
||||||
|
|
||||||
### Blockers/Concerns
|
### Blockers/Concerns
|
||||||
|
|
||||||
**None - Project Complete!**
|
**None - Project Complete!**
|
||||||
|
|
||||||
All 7 phases finished. System is production-ready with 4 minor UX/notification issues documented for future updates (see Deferred Issues above).
|
All 7 phases finished. All identified UX/notification issues resolved. System is fully production-ready.
|
||||||
|
|
||||||
**Production deployment:** Ready to proceed following `.planning/phases/07-testing-polish/DEPLOYMENT-CHECKLIST.md`
|
**Production deployment:** Ready to proceed following `.planning/phases/07-testing-polish/DEPLOYMENT-CHECKLIST.md`
|
||||||
|
|
||||||
|
### Quick Tasks Completed
|
||||||
|
|
||||||
|
| # | Description | Date | Commit | Directory |
|
||||||
|
|---|-------------|------|--------|-----------|
|
||||||
|
| 001 | UX & Notification Polish | 2026-01-29 | N/A | [001-ux-notification-polish](./quick/001-ux-notification-polish/) |
|
||||||
|
| 002 | Fix duplicate mentor notifications on job republish | 2026-01-29 | 4145a92 | [002-fix-duplicate-mentor-notifications-on-jo](./quick/002-fix-duplicate-mentor-notifications-on-jo/) |
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-01-29
|
Last session: 2026-01-29
|
||||||
Stopped at: Completed Plan 07-03 (Admin Flow & Deployment Prep) - Phase 7 complete (3/3 plans done)
|
Stopped at: Completed Quick Task 002: Fix duplicate mentor notifications on job republish
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|
||||||
**Project Status:** ✅ COMPLETE - All 7 phases finished, system ready for production deployment
|
**Project Status:** ✅ COMPLETE - All 7 phases finished, all polish items complete, system ready for production deployment
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
phase: quick-001
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- includes/class-notifications.php
|
||||||
|
- includes/class-pages.php
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Logged-in providers visiting /anbieter-login/ see a logout option instead of login/registration forms"
|
||||||
|
- "Admin submission email includes the job description text"
|
||||||
|
- "Admin deactivation email shows the actual deactivation reason entered by the provider"
|
||||||
|
- "Admin email edit/view links are clickable HTML hyperlinks"
|
||||||
|
artifacts:
|
||||||
|
- path: "includes/class-notifications.php"
|
||||||
|
provides: "Fixed admin email templates with HTML links, job description, and deactivation reason timing fix"
|
||||||
|
- path: "includes/class-pages.php"
|
||||||
|
provides: "Login page with logged-in provider detection and logout option"
|
||||||
|
key_links:
|
||||||
|
- from: "includes/class-notifications.php"
|
||||||
|
to: "ddhh_job_deactivated action"
|
||||||
|
via: "Hook into ddhh_job_deactivated instead of transition_post_status for deactivation emails"
|
||||||
|
pattern: "ddhh_job_deactivated"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Fix 4 UX/notification issues discovered during Phase 7 testing:
|
||||||
|
1. Add logout option on login page for already-logged-in providers
|
||||||
|
2. Include job description in admin submission email
|
||||||
|
3. Fix deactivation reason not appearing in admin notification (timing bug)
|
||||||
|
4. Make admin email edit links clickable HTML hyperlinks
|
||||||
|
|
||||||
|
Purpose: Polish UX and notification quality before production deployment.
|
||||||
|
Output: Updated class-notifications.php and class-pages.php with all 4 fixes.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@~/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@includes/class-notifications.php
|
||||||
|
@includes/class-pages.php
|
||||||
|
@includes/class-formidable.php (lines 700-765: deactivation handler showing meta save AFTER wp_update_post)
|
||||||
|
@includes/class-acf-fields.php (lines 82-99: field name is job_deactivation_reason)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Fix login page for logged-in providers</name>
|
||||||
|
<files>includes/class-pages.php</files>
|
||||||
|
<action>
|
||||||
|
The login page content is static HTML created during activation via `create_login_page()`. Since the page content includes `wp_login_form()` output baked in, we need a dynamic approach.
|
||||||
|
|
||||||
|
Add a new method and hook to `DDHH_JM_Pages`:
|
||||||
|
|
||||||
|
1. Add a `setup_hooks()` static method that registers a `template_redirect` hook for the login page.
|
||||||
|
|
||||||
|
2. In the `template_redirect` callback (`maybe_redirect_logged_in_from_login`):
|
||||||
|
- Check if current page is the login page (`get_option('ddhh_jm_login_page_id')`)
|
||||||
|
- If user is logged in AND has `ddhh_provider` role: redirect to dashboard page (`get_option('ddhh_jm_dashboard_page_id')`)
|
||||||
|
- If user is logged in but NOT a provider: do nothing (let them see the page as-is)
|
||||||
|
- If user is not logged in: do nothing (show login/registration forms)
|
||||||
|
|
||||||
|
3. Register `setup_hooks()` call in the class. The main plugin orchestrator (`class-ddhh-job-manager.php`) calls static setup methods, so add `DDHH_JM_Pages::setup_hooks()` there if not already called, OR make `setup_hooks()` callable from the existing init pattern.
|
||||||
|
|
||||||
|
NOTE: Check `class-ddhh-job-manager.php` for how Pages is initialized. If Pages has no `setup_hooks()` yet, add one and register it in the main plugin class `init_hooks()` method.
|
||||||
|
|
||||||
|
This approach is simpler than modifying static page content - logged-in providers simply get redirected to their dashboard (where they already have a logout link).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Log in as provider, visit /anbieter-login/ -- should redirect to /anbieter-dashboard/
|
||||||
|
- Visit /anbieter-login/ while logged out -- should show login/registration forms normally
|
||||||
|
- Grep for `maybe_redirect_logged_in_from_login` in class-pages.php confirms method exists
|
||||||
|
</verify>
|
||||||
|
<done>Logged-in providers are redirected from login page to dashboard; non-logged-in users see login/registration forms as before</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Fix all 3 admin email issues in notifications</name>
|
||||||
|
<files>includes/class-notifications.php</files>
|
||||||
|
<action>
|
||||||
|
Three fixes in class-notifications.php:
|
||||||
|
|
||||||
|
**Fix A: Add job description to admin submission email (`send_admin_new_job_notification_after_submit`)**
|
||||||
|
|
||||||
|
After line 67 (where job_contact_email is fetched), add:
|
||||||
|
```php
|
||||||
|
$job_description = $post->post_content;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the email body sprintf, add a "Beschreibung:" section after the contact email line and before the submission date. Truncate to 500 characters with "..." suffix if longer, to keep emails readable:
|
||||||
|
```php
|
||||||
|
$description_text = wp_strip_all_tags( $job_description );
|
||||||
|
if ( strlen( $description_text ) > 500 ) {
|
||||||
|
$description_text = substr( $description_text, 0, 500 ) . '...';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Add `"Beschreibung:\n%s\n\n"` to the format string and `$description_text` as parameter.
|
||||||
|
|
||||||
|
Also add job description to the EDIT notification (`send_admin_job_edit_notification_after_submit`) using the same pattern.
|
||||||
|
|
||||||
|
**Fix B: Fix deactivation reason timing bug (`send_admin_job_deactivation_notification`)**
|
||||||
|
|
||||||
|
ROOT CAUSE: The `transition_post_status` hook fires when `wp_update_post()` changes status to draft (class-formidable.php line 742). But the deactivation reason meta is saved AFTER that (line 756-758). So `get_post_meta($post->ID, 'job_deactivation_reason', true)` returns empty at notification time.
|
||||||
|
|
||||||
|
SOLUTION: Change the deactivation notification to hook into `ddhh_job_deactivated` action instead of `transition_post_status`. The `ddhh_job_deactivated` action fires AFTER meta is saved (class-formidable.php line 764).
|
||||||
|
|
||||||
|
In `setup_hooks()`:
|
||||||
|
- REMOVE: `add_action( 'transition_post_status', array( __CLASS__, 'send_admin_job_deactivation_notification' ), 10, 3 );`
|
||||||
|
- ADD: `add_action( 'ddhh_job_deactivated', array( __CLASS__, 'send_admin_job_deactivation_notification' ), 10, 2 );`
|
||||||
|
|
||||||
|
Update `send_admin_job_deactivation_notification` method signature:
|
||||||
|
- OLD: `( $new_status, $old_status, $post )`
|
||||||
|
- NEW: `( $post_id, $entry_id )`
|
||||||
|
|
||||||
|
Update method body:
|
||||||
|
- Get post via `get_post( $post_id )` at the top
|
||||||
|
- Remove the status transition checks (lines 233-240) since the action only fires on deactivation
|
||||||
|
- Keep all the rest (meta retrieval, email building, sending)
|
||||||
|
- The `get_post_meta( $post->ID, 'job_deactivation_reason', true )` will now work because meta is already saved
|
||||||
|
|
||||||
|
**Fix C: Make admin email links clickable HTML hyperlinks**
|
||||||
|
|
||||||
|
All three admin email methods use this pattern:
|
||||||
|
```php
|
||||||
|
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
||||||
|
$html_body = nl2br( esc_html( $body ) );
|
||||||
|
```
|
||||||
|
|
||||||
|
The problem: `esc_html()` escapes the URL but does NOT wrap it in `<a>` tags. The Content-Type says HTML but the links are plain text.
|
||||||
|
|
||||||
|
For each of the three admin notification methods (`send_admin_new_job_notification_after_submit`, `send_admin_job_edit_notification_after_submit`, `send_admin_job_deactivation_notification`):
|
||||||
|
|
||||||
|
Replace the plain text link pattern with proper HTML. Instead of building plain text and converting with nl2br(esc_html()), build the email body as proper HTML from the start:
|
||||||
|
|
||||||
|
Change the email body construction to use HTML directly. Replace the plain text sprintf approach with an HTML template approach:
|
||||||
|
- Use `<br>` instead of `\n`
|
||||||
|
- Wrap the edit link in `<a href="...">` tag: `<a href="' . esc_url( $edit_link ) . '">Stellenangebot pruefen</a>`
|
||||||
|
- Remove the `nl2br( esc_html( $body ) )` conversion -- assign the HTML body directly
|
||||||
|
- Use `esc_html()` on individual data values (job title, author name, etc.) but NOT on the entire body
|
||||||
|
- Keep the `Content-Type: text/html; charset=UTF-8` header
|
||||||
|
|
||||||
|
Example pattern for the link section:
|
||||||
|
```php
|
||||||
|
'<br><br><a href="' . esc_url( $edit_link ) . '">Stellenangebot in WordPress bearbeiten</a>'
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Grep class-notifications.php for `ddhh_job_deactivated` -- should appear in setup_hooks
|
||||||
|
- Grep class-notifications.php for `transition_post_status` -- should only appear once (for mentor publish notification, NOT for deactivation)
|
||||||
|
- Grep class-notifications.php for `esc_url.*edit_link` -- confirms links use esc_url in href attributes
|
||||||
|
- Grep class-notifications.php for `Beschreibung` -- confirms job description added to email
|
||||||
|
- Grep class-notifications.php for `<a href` -- confirms clickable links exist
|
||||||
|
- No `nl2br( esc_html(` pattern should remain in admin email methods (only in provider application email which is separate)
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- Admin submission/edit emails include job description (truncated to 500 chars)
|
||||||
|
- Deactivation notification hooks into ddhh_job_deactivated (fires after meta save) so reason is populated
|
||||||
|
- All admin email links are clickable HTML hyperlinks using esc_url() in href attributes
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
After both tasks complete:
|
||||||
|
1. `grep -c 'ddhh_job_deactivated' includes/class-notifications.php` returns 1 (in setup_hooks)
|
||||||
|
2. `grep -c 'transition_post_status' includes/class-notifications.php` returns 1 (only mentor publish hook)
|
||||||
|
3. `grep -c '<a href' includes/class-notifications.php` returns 3+ (one per admin email method)
|
||||||
|
4. `grep -c 'Beschreibung' includes/class-notifications.php` returns 2 (submission + edit emails)
|
||||||
|
5. `grep -c 'maybe_redirect_logged_in_from_login' includes/class-pages.php` returns 2+ (method definition + hook registration)
|
||||||
|
6. PHP syntax check: `php -l includes/class-notifications.php && php -l includes/class-pages.php`
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Logged-in providers are redirected away from /anbieter-login/ to dashboard
|
||||||
|
- Admin submission emails contain the job description text
|
||||||
|
- Admin deactivation emails display the actual deactivation reason (not "Kein Grund angegeben")
|
||||||
|
- All admin email links are clickable HTML hyperlinks
|
||||||
|
- No PHP syntax errors in modified files
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/quick/001-fix-4-ux-notification-issues-from-phase/001-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
phase: quick-001
|
||||||
|
plan: 01
|
||||||
|
subsystem: notifications
|
||||||
|
tags: [wordpress, email, formidable-forms, ux]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 02-provider-registration
|
||||||
|
provides: Login page infrastructure
|
||||||
|
- phase: 03-job-submission
|
||||||
|
provides: Admin notification system
|
||||||
|
- phase: 04-job-deactivation
|
||||||
|
provides: Deactivation workflow and ddhh_job_deactivated hook
|
||||||
|
provides:
|
||||||
|
- Logged-in provider redirect from login page to dashboard
|
||||||
|
- Job descriptions in admin submission/edit emails
|
||||||
|
- Deactivation reason appearing in admin emails (timing bug fixed)
|
||||||
|
- Clickable HTML links in all admin emails
|
||||||
|
affects: [production-deployment, admin-experience, provider-ux]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- Hook timing: ddhh_job_deactivated fires after meta save, ensuring data availability
|
||||||
|
- Login page redirect: template_redirect hook for dynamic behavior on static pages
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- includes/class-notifications.php
|
||||||
|
- includes/class-pages.php
|
||||||
|
- includes/class-ddhh-job-manager.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Hook into ddhh_job_deactivated instead of transition_post_status for deactivation emails to ensure meta fields are saved first"
|
||||||
|
- "Redirect logged-in providers from login page to dashboard using template_redirect hook rather than modifying static page content"
|
||||||
|
- "Build admin emails as proper HTML with esc_url() wrapped links instead of plain text conversion"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Admin email template pattern: Build as HTML string with esc_html() on values and esc_url() on links"
|
||||||
|
- "Login page dynamic behavior: Use template_redirect hook to handle logged-in state without altering static page content"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Task 001: UX & Notification Polish
|
||||||
|
|
||||||
|
**Fixed 4 UX/notification issues: provider login redirect, admin emails with descriptions, clickable links, and deactivation reason timing bug**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-29T03:57:55Z
|
||||||
|
- **Completed:** 2026-01-29T04:00:52Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 3
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Logged-in providers automatically redirected from login page to dashboard
|
||||||
|
- Admin submission/edit emails include job description (truncated to 500 chars)
|
||||||
|
- Deactivation reason now appears in admin notification (fixed timing bug)
|
||||||
|
- All admin email links are clickable HTML hyperlinks
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Fix login page for logged-in providers** - `84a4ae7` (feat)
|
||||||
|
2. **Task 2: Fix all 3 admin email issues in notifications** - `3dab3f9` (fix)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `includes/class-pages.php` - Added setup_hooks() and maybe_redirect_logged_in_from_login() for provider redirect
|
||||||
|
- `includes/class-ddhh-job-manager.php` - Registered Pages::setup_hooks() in plugin initialization
|
||||||
|
- `includes/class-notifications.php` - Fixed admin email HTML formatting, added job descriptions, fixed deactivation hook timing
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
**1. Hook timing for deactivation emails**
|
||||||
|
Changed from `transition_post_status` to `ddhh_job_deactivated` hook. The deactivation meta fields are saved AFTER `wp_update_post()` changes status (class-formidable.php lines 742-758). Using the custom action (fired at line 764) ensures meta is available when building the email.
|
||||||
|
|
||||||
|
**2. Dynamic login page behavior via template_redirect**
|
||||||
|
Instead of modifying static page content (which includes hardcoded login forms), used `template_redirect` hook to detect logged-in providers and redirect before page renders. Simpler and more maintainable than dynamic content generation.
|
||||||
|
|
||||||
|
**3. HTML email format with proper escaping**
|
||||||
|
Replaced `nl2br(esc_html($body))` pattern with direct HTML construction using `esc_html()` on individual data values and `esc_url()` on link hrefs. Makes links clickable while maintaining security.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None - all fixes implemented smoothly following the specifications in the plan.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
All 4 UX/notification issues from Phase 7 testing are now resolved:
|
||||||
|
- ✅ Logout option on login page (via redirect)
|
||||||
|
- ✅ Admin submission email includes job description
|
||||||
|
- ✅ Deactivation reason appears in admin email
|
||||||
|
- ✅ Admin email edit links are clickable
|
||||||
|
|
||||||
|
Ready for production deployment following `.planning/phases/07-testing-polish/DEPLOYMENT-CHECKLIST.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: quick-001*
|
||||||
|
*Completed: 2026-01-29*
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
phase: quick-002
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- includes/class-notifications.php
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Mentors receive notification when a job is first published"
|
||||||
|
- "Mentors do NOT receive notification when a previously-published job is edited and republished"
|
||||||
|
- "Each job can only trigger mentor notifications once in its lifetime"
|
||||||
|
artifacts:
|
||||||
|
- path: "includes/class-notifications.php"
|
||||||
|
provides: "Duplicate notification guard using post meta"
|
||||||
|
contains: "_ddhh_mentors_notified"
|
||||||
|
key_links:
|
||||||
|
- from: "notify_mentors_on_job_publish"
|
||||||
|
to: "post_meta _ddhh_mentors_notified"
|
||||||
|
via: "get_post_meta check before scheduling, update_post_meta after scheduling"
|
||||||
|
pattern: "get_post_meta.*_ddhh_mentors_notified"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Fix duplicate mentor notifications when a job is edited and republished.
|
||||||
|
|
||||||
|
Purpose: Mentors currently receive "new job" notifications every time a job transitions to
|
||||||
|
publish status -- including after edits (pending -> publish). The notification should only
|
||||||
|
fire once per job, on the very first publication. Subsequent republications after edits
|
||||||
|
should be silent for mentors (the admin already gets a separate edit notification).
|
||||||
|
|
||||||
|
Output: Updated `notify_mentors_on_job_publish` method with a post meta guard that prevents
|
||||||
|
duplicate notifications.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@~/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@includes/class-notifications.php
|
||||||
|
@includes/class-scheduler.php
|
||||||
|
|
||||||
|
The bug is in `notify_mentors_on_job_publish()` (line ~430). The current guard:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ( 'publish' !== $new_status || 'publish' === $old_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This correctly prevents re-notification when a published post is updated (publish -> publish),
|
||||||
|
but it does NOT prevent re-notification when a previously-published job is edited and reset
|
||||||
|
to pending (per decision 03-02: "Post status reset to pending after edit"), then republished
|
||||||
|
by admin (pending -> publish). This second publish triggers notifications again.
|
||||||
|
|
||||||
|
The job lifecycle that causes the bug:
|
||||||
|
1. Provider submits -> status: pending
|
||||||
|
2. Admin publishes -> pending to publish -> mentors notified (CORRECT)
|
||||||
|
3. Provider edits -> status reset to pending (per 03-02)
|
||||||
|
4. Admin republishes -> pending to publish -> mentors notified AGAIN (BUG)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add post meta guard to prevent duplicate mentor notifications</name>
|
||||||
|
<files>includes/class-notifications.php</files>
|
||||||
|
<action>
|
||||||
|
In the `notify_mentors_on_job_publish` method, add a post meta check AFTER the existing
|
||||||
|
status transition guard (line ~437) and BEFORE the mentor query (line ~442):
|
||||||
|
|
||||||
|
1. Check if post meta `_ddhh_mentors_notified` exists and equals `'1'` for this post.
|
||||||
|
If it does, log a message like "DDHH Job Manager: Skipping mentor notification for
|
||||||
|
job #%d - mentors already notified on initial publish" and return early.
|
||||||
|
|
||||||
|
2. After the `DDHH_JM_Scheduler::schedule_mentor_notification_batch()` call succeeds
|
||||||
|
(line ~457), set post meta: `update_post_meta( $post->ID, '_ddhh_mentors_notified', '1' )`.
|
||||||
|
|
||||||
|
Use underscore-prefixed meta key `_ddhh_mentors_notified` so it is hidden from the
|
||||||
|
WordPress custom fields UI.
|
||||||
|
|
||||||
|
Do NOT change any other logic in this method or file. The existing status transition
|
||||||
|
guard should remain as-is (it still serves a purpose for publish->publish transitions).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. Read the modified file and confirm:
|
||||||
|
- `get_post_meta( $post->ID, '_ddhh_mentors_notified', true )` check exists before mentor query
|
||||||
|
- `update_post_meta( $post->ID, '_ddhh_mentors_notified', '1' )` exists after scheduling
|
||||||
|
- No other methods were modified
|
||||||
|
2. Run `php -l includes/class-notifications.php` to confirm no syntax errors
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
The `notify_mentors_on_job_publish` method checks for `_ddhh_mentors_notified` post meta
|
||||||
|
before scheduling notifications, and sets that meta after scheduling. This ensures mentors
|
||||||
|
are only notified once per job, regardless of how many times the job transitions through
|
||||||
|
pending -> publish.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `php -l includes/class-notifications.php` passes with no syntax errors
|
||||||
|
2. The `_ddhh_mentors_notified` meta key is checked before notification scheduling
|
||||||
|
3. The `_ddhh_mentors_notified` meta key is set after successful scheduling
|
||||||
|
4. No other notification methods were modified
|
||||||
|
5. The existing `transition_post_status` guard logic is preserved
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- First publish of a job (pending -> publish) triggers mentor notifications and sets meta flag
|
||||||
|
- Subsequent publishes of the same job (pending -> publish after edit) skip mentor notifications
|
||||||
|
- Admin notifications for edits/republications are unaffected
|
||||||
|
- No PHP syntax errors introduced
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/quick/002-fix-duplicate-mentor-notifications-on-jo/002-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
phase: quick-002
|
||||||
|
plan: 01
|
||||||
|
subsystem: notifications
|
||||||
|
tags: [mentor-notifications, duplicate-prevention, post-meta, bug-fix]
|
||||||
|
requires: [05-01, 05-03]
|
||||||
|
provides:
|
||||||
|
- Duplicate notification prevention using post meta
|
||||||
|
- One-time mentor notification guarantee per job
|
||||||
|
affects: []
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Post meta flags for one-time event tracking"
|
||||||
|
- "Idempotent notification scheduling"
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- includes/class-notifications.php
|
||||||
|
decisions:
|
||||||
|
- key: "Use post meta `_ddhh_mentors_notified` for duplicate prevention"
|
||||||
|
context: "Need to track whether mentors have been notified for a specific job"
|
||||||
|
options: ["Post meta flag", "Custom database table", "Transient cache"]
|
||||||
|
chosen: "Post meta flag"
|
||||||
|
rationale: "Simple, reliable, persists forever, underscore-prefix hides from UI"
|
||||||
|
- key: "Set meta flag after scheduling, not after emails sent"
|
||||||
|
context: "When to mark job as notified"
|
||||||
|
options: ["After scheduling batch", "After all emails sent", "Before scheduling"]
|
||||||
|
chosen: "After scheduling batch"
|
||||||
|
rationale: "Scheduling is synchronous and reliable; email sending is async and could fail individually"
|
||||||
|
metrics:
|
||||||
|
duration: 2
|
||||||
|
completed: 2026-01-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Task 002: Fix Duplicate Mentor Notifications
|
||||||
|
|
||||||
|
**One-liner:** Post meta guard prevents mentor re-notification on job republish after edits
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
Fixed a bug where mentors received duplicate "new job" notifications when a job was edited and republished.
|
||||||
|
|
||||||
|
**The Problem:**
|
||||||
|
The job lifecycle includes a status reset to `pending` after provider edits (decision 03-02). This means:
|
||||||
|
1. Provider submits → pending
|
||||||
|
2. Admin publishes → pending to publish → mentors notified ✓
|
||||||
|
3. Provider edits → status reset to pending
|
||||||
|
4. Admin republishes → pending to publish → mentors notified AGAIN ✗
|
||||||
|
|
||||||
|
The existing guard `if ( 'publish' === $old_status )` only prevented publish→publish transitions, not pending→publish after edits.
|
||||||
|
|
||||||
|
**The Solution:**
|
||||||
|
Added a post meta flag `_ddhh_mentors_notified` that:
|
||||||
|
- Is checked before scheduling notifications (early return if already set)
|
||||||
|
- Is set after successful batch scheduling
|
||||||
|
- Persists for the lifetime of the job post
|
||||||
|
- Uses underscore prefix to hide from WordPress custom fields UI
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
**Modified:** `includes/class-notifications.php`
|
||||||
|
|
||||||
|
1. **Pre-scheduling guard** (after line 439):
|
||||||
|
- `get_post_meta( $post->ID, '_ddhh_mentors_notified', true )`
|
||||||
|
- If value is `'1'`, log skip message and return early
|
||||||
|
- Prevents duplicate scheduling entirely
|
||||||
|
|
||||||
|
2. **Post-scheduling flag** (after line 469):
|
||||||
|
- `update_post_meta( $post->ID, '_ddhh_mentors_notified', '1' )`
|
||||||
|
- Sets flag after `schedule_mentor_notification_batch()` succeeds
|
||||||
|
- Ensures future republish attempts skip notification
|
||||||
|
|
||||||
|
**Why after scheduling, not after sending?**
|
||||||
|
- Scheduling is synchronous and happens immediately
|
||||||
|
- Email sending is async (Action Scheduler batches)
|
||||||
|
- Individual emails might fail, but we still don't want re-notifications
|
||||||
|
- The intent to notify has been recorded, which is what matters
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
**Syntax Check:**
|
||||||
|
```bash
|
||||||
|
php -l includes/class-notifications.php
|
||||||
|
# No syntax errors detected
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Review:**
|
||||||
|
- ✅ `get_post_meta` check exists before mentor query
|
||||||
|
- ✅ `update_post_meta` exists after scheduling
|
||||||
|
- ✅ No other notification methods modified
|
||||||
|
- ✅ Existing publish→publish guard preserved
|
||||||
|
- ✅ Underscore-prefixed meta key (hidden from UI)
|
||||||
|
|
||||||
|
## Job Lifecycle Flow (After Fix)
|
||||||
|
|
||||||
|
1. **Initial submission** (pending):
|
||||||
|
- No notification (job not published yet)
|
||||||
|
- `_ddhh_mentors_notified` = not set
|
||||||
|
|
||||||
|
2. **First publish** (pending → publish):
|
||||||
|
- ✅ Mentors notified
|
||||||
|
- `_ddhh_mentors_notified` = '1'
|
||||||
|
|
||||||
|
3. **Provider edits** (publish → pending):
|
||||||
|
- No notification (deactivation)
|
||||||
|
- `_ddhh_mentors_notified` = still '1'
|
||||||
|
|
||||||
|
4. **Republish after edit** (pending → publish):
|
||||||
|
- ❌ Mentors NOT notified (meta flag prevents it)
|
||||||
|
- `_ddhh_mentors_notified` = still '1'
|
||||||
|
|
||||||
|
5. **Any future republish**:
|
||||||
|
- ❌ Mentors NOT notified (meta flag persists)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**Fixed:**
|
||||||
|
- Mentors no longer receive duplicate notifications for the same job
|
||||||
|
- Reduces notification fatigue and confusion
|
||||||
|
- Maintains trust in the notification system
|
||||||
|
|
||||||
|
**Unchanged:**
|
||||||
|
- Admin notifications for edits (still sent, as intended)
|
||||||
|
- First-time publish notifications (still sent)
|
||||||
|
- Publish→publish guard (still prevents updates to published jobs)
|
||||||
|
- Mentor opt-in/opt-out functionality
|
||||||
|
|
||||||
|
**Edge Cases Handled:**
|
||||||
|
- Job edited multiple times: Only first publish notifies
|
||||||
|
- Job unpublished and republished: Only first publish notifies
|
||||||
|
- Job trashed and restored: Meta persists, no re-notification
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
**To verify in production:**
|
||||||
|
|
||||||
|
1. Create test job, submit as provider → status: pending
|
||||||
|
2. Admin publishes → Check Action Scheduler for mentor notification batches
|
||||||
|
3. Provider edits job → status: pending
|
||||||
|
4. Admin republishes → Check Action Scheduler - should see NO new batches
|
||||||
|
5. Check error_log for: "Skipping mentor notification for job #X - mentors already notified on initial publish"
|
||||||
|
|
||||||
|
**Manual database check:**
|
||||||
|
```sql
|
||||||
|
SELECT post_id, meta_value
|
||||||
|
FROM wp_postmeta
|
||||||
|
WHERE meta_key = '_ddhh_mentors_notified';
|
||||||
|
```
|
||||||
|
|
||||||
|
Should show '1' for all published jobs that have triggered notifications.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Blockers:** None
|
||||||
|
**Dependencies satisfied:** Builds on existing notification system (05-01, 05-03)
|
||||||
|
**Follow-up needed:** None - fix is complete and self-contained
|
||||||
|
|
||||||
|
This was a targeted bug fix with no architectural implications. The notification system is now fully idempotent - each job triggers exactly one mentor notification batch in its lifetime.
|
||||||
225
CLAUDE.md
225
CLAUDE.md
@@ -31,131 +31,6 @@ Common commands:
|
|||||||
- `/gsd:execute-phase` - Execute all plans in a phase
|
- `/gsd:execute-phase` - Execute all plans in a phase
|
||||||
- `/gsd:help` - View all available GSD commands
|
- `/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
|
## Development Workflow
|
||||||
|
|
||||||
### Testing Locally
|
### Testing Locally
|
||||||
@@ -187,91 +62,23 @@ Email templates are inline in `class-notifications.php`:
|
|||||||
3. Hook into appropriate action in `setup_hooks()`
|
3. Hook into appropriate action in `setup_hooks()`
|
||||||
4. For batch processing, use `DDHH_JM_Scheduler::schedule_mentor_notification_batch()`
|
4. For batch processing, use `DDHH_JM_Scheduler::schedule_mentor_notification_batch()`
|
||||||
|
|
||||||
## Common Tasks
|
### CSS Best Practices
|
||||||
|
|
||||||
### Debugging Action Scheduler
|
**NEVER use `!important` in CSS.** This is a bad practice that creates maintenance issues and specificity wars.
|
||||||
|
|
||||||
```bash
|
Instead, use proper CSS specificity to override styles:
|
||||||
# View scheduled actions in WP CLI
|
- **Bad:** `.button { color: #fff !important; }`
|
||||||
wp action-scheduler list --status=pending
|
- **Good:** `.ddhh-jobs-table .button { color: #fff; }`
|
||||||
wp action-scheduler list --status=failed
|
|
||||||
|
|
||||||
# Run pending actions manually
|
Specificity hierarchy (from weakest to strongest):
|
||||||
wp action-scheduler run --batch-size=10
|
1. Element selectors: `button { }`
|
||||||
|
2. Class selectors: `.button { }`
|
||||||
|
3. Multiple classes: `.ddhh-jobs-table .button { }`
|
||||||
|
4. ID selectors: `#my-id { }` (avoid in most cases)
|
||||||
|
5. Inline styles: `style="..."` (avoid)
|
||||||
|
6. `!important` (NEVER use unless absolutely necessary for overriding external libraries)
|
||||||
|
|
||||||
# Clean up old actions
|
**Template-specific styles:**
|
||||||
wp action-scheduler clean --days=90
|
- All dashboard template styles are scoped with `.ddhh-provider-dashboard` or child selectors
|
||||||
```
|
- This prevents conflicts with Elementor and other theme styles
|
||||||
|
- Use parent selector chains like `.ddhh-jobs-table .button` for higher specificity
|
||||||
### 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
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: Digital Dabei Job Manager
|
* Plugin Name: Digital Dabei Job Manager
|
||||||
* Plugin URI: https://www.hamburg.de/digital-dabei
|
* Plugin URI: https://hamburg-digital-dabei.de
|
||||||
* Description: Closed job board for provider self-registration and mentor applications
|
* Description: Closed job board for provider self-registration and mentor applications
|
||||||
* Version: 1.0.0
|
* Version: 1.0.0
|
||||||
* Author: digital dabei Hamburg
|
* Author: vihais
|
||||||
* Author URI: https://www.hamburg.de/digital-dabei
|
* Author URI: https://hamburg-digital-dabei.de
|
||||||
* Text Domain: ddhh-job-manager
|
* Text Domain: ddhh-job-manager
|
||||||
* Domain Path: /languages
|
* Domain Path: /languages
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
@@ -45,8 +45,13 @@ 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';
|
||||||
|
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-elementor-tags.php';
|
||||||
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-ddhh-job-manager.php';
|
require_once DDHH_JM_PLUGIN_DIR . 'includes/class-ddhh-job-manager.php';
|
||||||
|
|
||||||
|
// Register activation and deactivation hooks (must be at top level).
|
||||||
|
register_activation_hook( __FILE__, array( 'DDHH_JM_Activator', 'activate' ) );
|
||||||
|
register_deactivation_hook( __FILE__, array( 'DDHH_JM_Deactivator', 'deactivate' ) );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the plugin.
|
* Initialize the plugin.
|
||||||
*/
|
*/
|
||||||
@@ -54,3 +59,11 @@ function ddhh_jm_init() {
|
|||||||
DDHH_JM_Job_Manager::get_instance();
|
DDHH_JM_Job_Manager::get_instance();
|
||||||
}
|
}
|
||||||
add_action( 'plugins_loaded', 'ddhh_jm_init', 10 );
|
add_action( 'plugins_loaded', 'ddhh_jm_init', 10 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Elementor dynamic tags when Elementor is loaded.
|
||||||
|
*/
|
||||||
|
function ddhh_jm_elementor_init() {
|
||||||
|
DDHH_JM_Elementor_Tags::init();
|
||||||
|
}
|
||||||
|
add_action( 'elementor/init', 'ddhh_jm_elementor_init' );
|
||||||
|
|||||||
@@ -68,17 +68,6 @@ class DDHH_JM_ACF_Fields {
|
|||||||
'type' => 'email',
|
'type' => 'email',
|
||||||
'required' => 1,
|
'required' => 1,
|
||||||
),
|
),
|
||||||
// Job Logo
|
|
||||||
array(
|
|
||||||
'key' => 'field_job_logo',
|
|
||||||
'label' => __( 'Logo', 'ddhh-job-manager' ),
|
|
||||||
'name' => 'job_logo',
|
|
||||||
'type' => 'image',
|
|
||||||
'required' => 0,
|
|
||||||
'return_format' => 'id',
|
|
||||||
'preview_size' => 'thumbnail',
|
|
||||||
'library' => 'all',
|
|
||||||
),
|
|
||||||
// Job Deactivation Reason (internal, admin-only)
|
// Job Deactivation Reason (internal, admin-only)
|
||||||
array(
|
array(
|
||||||
'key' => 'field_job_deactivation_reason',
|
'key' => 'field_job_deactivation_reason',
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ class DDHH_JM_Job_Manager {
|
|||||||
* Initialize hooks
|
* Initialize hooks
|
||||||
*/
|
*/
|
||||||
private function init_hooks() {
|
private function init_hooks() {
|
||||||
// Register activation and deactivation hooks
|
|
||||||
register_activation_hook( DDHH_JM_PLUGIN_FILE, array( 'DDHH_JM_Activator', 'activate' ) );
|
|
||||||
register_deactivation_hook( DDHH_JM_PLUGIN_FILE, array( 'DDHH_JM_Deactivator', 'deactivate' ) );
|
|
||||||
|
|
||||||
// Initialize post types
|
// Initialize post types
|
||||||
add_action( 'init', array( 'DDHH_JM_Post_Types', 'register' ) );
|
add_action( 'init', array( 'DDHH_JM_Post_Types', 'register' ) );
|
||||||
|
|
||||||
@@ -84,5 +80,8 @@ class DDHH_JM_Job_Manager {
|
|||||||
|
|
||||||
// Initialize scheduler for async email processing
|
// Initialize scheduler for async email processing
|
||||||
add_action( 'init', array( 'DDHH_JM_Scheduler', 'setup_hooks' ) );
|
add_action( 'init', array( 'DDHH_JM_Scheduler', 'setup_hooks' ) );
|
||||||
|
|
||||||
|
// Initialize pages
|
||||||
|
add_action( 'init', array( 'DDHH_JM_Pages', 'setup_hooks' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
includes/class-elementor-tags.php
Normal file
58
includes/class-elementor-tags.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor Dynamic Tags integration.
|
||||||
|
*
|
||||||
|
* Registers custom dynamic tags for job offer fields so they appear
|
||||||
|
* in the Elementor editor's dynamic tags dropdown under a
|
||||||
|
* "Stellenangebot" group.
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DDHH_JM_Elementor_Tags
|
||||||
|
*/
|
||||||
|
class DDHH_JM_Elementor_Tags {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group slug used for all job offer tags.
|
||||||
|
*/
|
||||||
|
const GROUP_SLUG = 'ddhh-job-offer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hooks.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action( 'elementor/dynamic_tags/register', array( __CLASS__, 'register_tags' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the dynamic tag group and individual tags.
|
||||||
|
*
|
||||||
|
* @param \Elementor\Core\DynamicTags\Manager $manager Elementor dynamic tags manager.
|
||||||
|
*/
|
||||||
|
public static function register_tags( $manager ) {
|
||||||
|
$manager->register_group(
|
||||||
|
self::GROUP_SLUG,
|
||||||
|
array(
|
||||||
|
'title' => 'Stellenangebot',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$tag_dir = DDHH_JM_PLUGIN_DIR . 'includes/elementor-tags/';
|
||||||
|
|
||||||
|
require_once $tag_dir . 'class-tag-job-location.php';
|
||||||
|
require_once $tag_dir . 'class-tag-job-type.php';
|
||||||
|
require_once $tag_dir . 'class-tag-job-deadline.php';
|
||||||
|
require_once $tag_dir . 'class-tag-job-contact-email.php';
|
||||||
|
require_once $tag_dir . 'class-tag-job-logo.php';
|
||||||
|
|
||||||
|
$manager->register( new DDHH_JM_Tag_Job_Location() );
|
||||||
|
$manager->register( new DDHH_JM_Tag_Job_Type() );
|
||||||
|
$manager->register( new DDHH_JM_Tag_Job_Deadline() );
|
||||||
|
$manager->register( new DDHH_JM_Tag_Job_Contact_Email() );
|
||||||
|
$manager->register( new DDHH_JM_Tag_Job_Logo() );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,6 +156,7 @@ class DDHH_JM_Formidable {
|
|||||||
add_action( 'init', array( __CLASS__, 'create_registration_form' ), 11 );
|
add_action( 'init', array( __CLASS__, 'create_registration_form' ), 11 );
|
||||||
add_action( 'init', array( __CLASS__, 'create_job_submission_form' ), 11 );
|
add_action( 'init', array( __CLASS__, 'create_job_submission_form' ), 11 );
|
||||||
add_action( 'init', array( __CLASS__, 'create_job_edit_form' ), 11 );
|
add_action( 'init', array( __CLASS__, 'create_job_edit_form' ), 11 );
|
||||||
|
add_action( 'init', array( __CLASS__, 'repair_job_form_fields' ), 12 );
|
||||||
add_action( 'init', array( __CLASS__, 'create_job_deactivation_form' ), 11 );
|
add_action( 'init', array( __CLASS__, 'create_job_deactivation_form' ), 11 );
|
||||||
add_action( 'init', array( __CLASS__, 'create_job_application_form' ), 11 );
|
add_action( 'init', array( __CLASS__, 'create_job_application_form' ), 11 );
|
||||||
|
|
||||||
@@ -474,7 +475,6 @@ class DDHH_JM_Formidable {
|
|||||||
$job_type = '';
|
$job_type = '';
|
||||||
$job_deadline = '';
|
$job_deadline = '';
|
||||||
$job_contact_email = '';
|
$job_contact_email = '';
|
||||||
$job_logo = '';
|
|
||||||
|
|
||||||
foreach ( $entry->metas as $field_id => $value ) {
|
foreach ( $entry->metas as $field_id => $value ) {
|
||||||
$field = FrmField::getOne( $field_id );
|
$field = FrmField::getOne( $field_id );
|
||||||
@@ -501,9 +501,6 @@ class DDHH_JM_Formidable {
|
|||||||
case 'job_contact_email':
|
case 'job_contact_email':
|
||||||
$job_contact_email = sanitize_email( $value );
|
$job_contact_email = sanitize_email( $value );
|
||||||
break;
|
break;
|
||||||
case 'job_logo':
|
|
||||||
$job_logo = $value; // File ID
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,12 +538,6 @@ class DDHH_JM_Formidable {
|
|||||||
update_post_meta( $post_id, 'job_deadline', $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' );
|
error_log( 'DDHH Job Submission: Job offer created successfully' );
|
||||||
|
|
||||||
do_action( 'ddhh_job_submitted', $post_id, $entry_id );
|
do_action( 'ddhh_job_submitted', $post_id, $entry_id );
|
||||||
@@ -599,7 +590,6 @@ class DDHH_JM_Formidable {
|
|||||||
$job_type = '';
|
$job_type = '';
|
||||||
$job_deadline = '';
|
$job_deadline = '';
|
||||||
$job_contact_email = '';
|
$job_contact_email = '';
|
||||||
$job_logo = '';
|
|
||||||
|
|
||||||
foreach ( $entry->metas as $field_id => $value ) {
|
foreach ( $entry->metas as $field_id => $value ) {
|
||||||
$field = FrmField::getOne( $field_id );
|
$field = FrmField::getOne( $field_id );
|
||||||
@@ -632,10 +622,6 @@ class DDHH_JM_Formidable {
|
|||||||
case 'job_contact_email2':
|
case 'job_contact_email2':
|
||||||
$job_contact_email = sanitize_email( $value );
|
$job_contact_email = sanitize_email( $value );
|
||||||
break;
|
break;
|
||||||
case 'job_logo':
|
|
||||||
case 'job_logo2':
|
|
||||||
$job_logo = $value; // File ID
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,12 +658,6 @@ class DDHH_JM_Formidable {
|
|||||||
update_post_meta( $post_id, 'job_deadline', $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' );
|
error_log( 'DDHH Job Edit: Job offer updated successfully' );
|
||||||
|
|
||||||
do_action( 'ddhh_job_edited', $post_id, $entry_id );
|
do_action( 'ddhh_job_edited', $post_id, $entry_id );
|
||||||
@@ -835,13 +815,7 @@ class DDHH_JM_Formidable {
|
|||||||
'required' => '1',
|
'required' => '1',
|
||||||
'form_id' => $form_id,
|
'form_id' => $form_id,
|
||||||
'field_order' => 4,
|
'field_order' => 4,
|
||||||
'field_options' => array(
|
'options' => array( '', 'Vollzeit', 'Teilzeit', 'Ehrenamt' ),
|
||||||
'options' => array(
|
|
||||||
'Vollzeit' => 'Vollzeit',
|
|
||||||
'Teilzeit' => 'Teilzeit',
|
|
||||||
'Ehrenamt' => 'Ehrenamt',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'name' => 'Bewerbungsfrist',
|
'name' => 'Bewerbungsfrist',
|
||||||
@@ -862,19 +836,6 @@ class DDHH_JM_Formidable {
|
|||||||
'form_id' => $form_id,
|
'form_id' => $form_id,
|
||||||
'field_order' => 6,
|
'field_order' => 6,
|
||||||
),
|
),
|
||||||
array(
|
|
||||||
'name' => 'Logo',
|
|
||||||
'field_key' => 'job_logo',
|
|
||||||
'type' => 'file',
|
|
||||||
'required' => '0',
|
|
||||||
'form_id' => $form_id,
|
|
||||||
'field_order' => 7,
|
|
||||||
'field_options' => array(
|
|
||||||
'restrict' => '1',
|
|
||||||
'allowed_types' => 'image/jpeg,image/png',
|
|
||||||
'max_size' => '2',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store field IDs for form action mapping
|
// Store field IDs for form action mapping
|
||||||
@@ -888,49 +849,20 @@ class DDHH_JM_Formidable {
|
|||||||
|
|
||||||
// Create the Create Post action
|
// Create the Create Post action
|
||||||
if ( ! empty( $field_ids ) ) {
|
if ( ! empty( $field_ids ) ) {
|
||||||
$action_values = array(
|
$action_control = FrmFormActionsController::get_form_actions( 'wppost' );
|
||||||
'menu_order' => 1,
|
$new_action = $action_control->prepare_new( $form_id );
|
||||||
'post_status' => 'published',
|
$new_action->post_content['post_type'] = 'job_offer';
|
||||||
'post_content' => array(
|
$new_action->post_content['post_status'] = 'pending';
|
||||||
'post_type' => 'job_offer',
|
$new_action->post_content['post_title'] = $field_ids['job_title'];
|
||||||
'post_status' => 'pending',
|
$new_action->post_content['post_content'] = $field_ids['job_description'];
|
||||||
'post_title' => $field_ids['job_title'],
|
$new_action->post_content['post_author'] = 'current_user';
|
||||||
'post_content' => $field_ids['job_description'],
|
$new_action->post_content['post_custom_fields'] = array(
|
||||||
'post_author' => 'current_user',
|
array( 'meta_name' => 'job_location', 'field_id' => $field_ids['job_location'] ),
|
||||||
'post_custom_fields' => array(
|
array( 'meta_name' => 'job_type', 'field_id' => $field_ids['job_type'] ),
|
||||||
array(
|
array( 'meta_name' => 'job_deadline', 'field_id' => $field_ids['job_deadline'] ),
|
||||||
'meta_name' => 'job_location',
|
array( 'meta_name' => 'job_contact_email', 'field_id' => $field_ids['job_contact_email'] ),
|
||||||
'field_id' => $field_ids['job_location'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_type',
|
|
||||||
'field_id' => $field_ids['job_type'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_deadline',
|
|
||||||
'field_id' => $field_ids['job_deadline'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_contact_email',
|
|
||||||
'field_id' => $field_ids['job_contact_email'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_logo',
|
|
||||||
'field_id' => $field_ids['job_logo'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the form action using the proper API
|
|
||||||
FrmFormActionsController::create_action(
|
|
||||||
$form_id,
|
|
||||||
array(
|
|
||||||
'post_excerpt' => 'wppost',
|
|
||||||
'post_content' => $action_values,
|
|
||||||
'menu_order' => 1,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$action_control->save_settings( $new_action );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,13 +937,7 @@ class DDHH_JM_Formidable {
|
|||||||
'required' => '1',
|
'required' => '1',
|
||||||
'form_id' => $form_id,
|
'form_id' => $form_id,
|
||||||
'field_order' => 4,
|
'field_order' => 4,
|
||||||
'field_options' => array(
|
'options' => array( '', 'Vollzeit', 'Teilzeit', 'Ehrenamt' ),
|
||||||
'options' => array(
|
|
||||||
'Vollzeit' => 'Vollzeit',
|
|
||||||
'Teilzeit' => 'Teilzeit',
|
|
||||||
'Ehrenamt' => 'Ehrenamt',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'name' => 'Bewerbungsfrist',
|
'name' => 'Bewerbungsfrist',
|
||||||
@@ -1032,19 +958,6 @@ class DDHH_JM_Formidable {
|
|||||||
'form_id' => $form_id,
|
'form_id' => $form_id,
|
||||||
'field_order' => 6,
|
'field_order' => 6,
|
||||||
),
|
),
|
||||||
array(
|
|
||||||
'name' => 'Logo',
|
|
||||||
'field_key' => 'job_logo',
|
|
||||||
'type' => 'file',
|
|
||||||
'required' => '0',
|
|
||||||
'form_id' => $form_id,
|
|
||||||
'field_order' => 7,
|
|
||||||
'field_options' => array(
|
|
||||||
'restrict' => '1',
|
|
||||||
'allowed_types' => 'image/jpeg,image/png',
|
|
||||||
'max_size' => '2',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store field IDs for form action mapping
|
// Store field IDs for form action mapping
|
||||||
@@ -1058,52 +971,62 @@ class DDHH_JM_Formidable {
|
|||||||
|
|
||||||
// Create the Update Post action
|
// Create the Update Post action
|
||||||
if ( ! empty( $field_ids ) ) {
|
if ( ! empty( $field_ids ) ) {
|
||||||
$action_values = array(
|
$action_control = FrmFormActionsController::get_form_actions( 'wppost' );
|
||||||
'menu_order' => 1,
|
$new_action = $action_control->prepare_new( $form_id );
|
||||||
'post_status' => 'published',
|
$new_action->post_content['post_type'] = 'job_offer';
|
||||||
'post_content' => array(
|
$new_action->post_content['post_status'] = 'pending';
|
||||||
'post_type' => 'job_offer',
|
$new_action->post_content['post_title'] = $field_ids['job_title'];
|
||||||
'post_status' => 'pending',
|
$new_action->post_content['post_content'] = $field_ids['job_description'];
|
||||||
'post_title' => $field_ids['job_title'],
|
$new_action->post_content['post_id'] = 'id_param';
|
||||||
'post_content' => $field_ids['job_description'],
|
$new_action->post_content['post_custom_fields'] = array(
|
||||||
'post_id' => 'id_param',
|
array( 'meta_name' => 'job_location', 'field_id' => $field_ids['job_location'] ),
|
||||||
'post_custom_fields' => array(
|
array( 'meta_name' => 'job_type', 'field_id' => $field_ids['job_type'] ),
|
||||||
array(
|
array( 'meta_name' => 'job_deadline', 'field_id' => $field_ids['job_deadline'] ),
|
||||||
'meta_name' => 'job_location',
|
array( 'meta_name' => 'job_contact_email', 'field_id' => $field_ids['job_contact_email'] ),
|
||||||
'field_id' => $field_ids['job_location'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_type',
|
|
||||||
'field_id' => $field_ids['job_type'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_deadline',
|
|
||||||
'field_id' => $field_ids['job_deadline'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_contact_email',
|
|
||||||
'field_id' => $field_ids['job_contact_email'],
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_logo',
|
|
||||||
'field_id' => $field_ids['job_logo'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the form action using the proper API
|
|
||||||
FrmFormActionsController::create_action(
|
|
||||||
$form_id,
|
|
||||||
array(
|
|
||||||
'post_excerpt' => 'wppost',
|
|
||||||
'post_content' => $action_values,
|
|
||||||
'menu_order' => 1,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$action_control->save_settings( $new_action );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repair existing job form fields in the database.
|
||||||
|
*
|
||||||
|
* Fixes the job_type select options and removes the job_logo field
|
||||||
|
* from both the submission and edit forms. Runs once and stores a
|
||||||
|
* version flag in wp_options to avoid re-running.
|
||||||
|
*/
|
||||||
|
public static function repair_job_form_fields() {
|
||||||
|
if ( ! class_exists( 'FrmField' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repair_version = '1';
|
||||||
|
if ( get_option( 'ddhh_jm_form_repair_version' ) === $repair_version ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix job_type options on both forms.
|
||||||
|
$correct_options = array( '', 'Vollzeit', 'Teilzeit', 'Ehrenamt' );
|
||||||
|
foreach ( array( 'job_type', 'job_type2' ) as $key ) {
|
||||||
|
$field = FrmField::getOne( $key );
|
||||||
|
if ( $field ) {
|
||||||
|
FrmField::update( $field->id, array(
|
||||||
|
'options' => serialize( $correct_options ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove job_logo fields from both forms.
|
||||||
|
foreach ( array( 'job_logo', 'job_logo2' ) as $key ) {
|
||||||
|
$field = FrmField::getOne( $key );
|
||||||
|
if ( $field ) {
|
||||||
|
FrmField::destroy( $field->id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option( 'ddhh_jm_form_repair_version', $repair_version );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre-populate edit form fields with existing post data
|
* Pre-populate edit form fields with existing post data
|
||||||
*
|
*
|
||||||
@@ -1159,11 +1082,6 @@ class DDHH_JM_Formidable {
|
|||||||
case 'job_contact_email2':
|
case 'job_contact_email2':
|
||||||
$value = get_post_meta( $post_id, 'job_contact_email', true );
|
$value = get_post_meta( $post_id, 'job_contact_email', true );
|
||||||
return $value ? $value : $default_value;
|
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;
|
return $default_value;
|
||||||
@@ -1277,31 +1195,15 @@ class DDHH_JM_Formidable {
|
|||||||
|
|
||||||
// Create the Update Post action
|
// Create the Update Post action
|
||||||
if ( ! empty( $field_ids ) ) {
|
if ( ! empty( $field_ids ) ) {
|
||||||
$action_values = array(
|
$action_control = FrmFormActionsController::get_form_actions( 'wppost' );
|
||||||
'menu_order' => 1,
|
$new_action = $action_control->prepare_new( $form_id );
|
||||||
'post_status' => 'published',
|
$new_action->post_content['post_type'] = 'job_offer';
|
||||||
'post_content' => array(
|
$new_action->post_content['post_status'] = 'draft';
|
||||||
'post_type' => 'job_offer',
|
$new_action->post_content['post_id'] = 'id_param';
|
||||||
'post_status' => 'draft',
|
$new_action->post_content['post_custom_fields'] = array(
|
||||||
'post_id' => 'id_param',
|
array( 'meta_name' => 'job_deactivation_reason', 'field_id' => $field_ids['deactivation_reason'] ),
|
||||||
'post_custom_fields' => array(
|
|
||||||
array(
|
|
||||||
'meta_name' => 'job_deactivation_reason',
|
|
||||||
'field_id' => $field_ids['deactivation_reason'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the form action using the proper API
|
|
||||||
FrmFormActionsController::create_action(
|
|
||||||
$form_id,
|
|
||||||
array(
|
|
||||||
'post_excerpt' => 'wppost',
|
|
||||||
'post_content' => $action_values,
|
|
||||||
'menu_order' => 1,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$action_control->save_settings( $new_action );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class DDHH_JM_Notifications {
|
|||||||
// Hook into job edit to send admin notification (after metadata is saved)
|
// Hook into job edit to send admin notification (after metadata is saved)
|
||||||
add_action( 'ddhh_job_edited', array( __CLASS__, 'send_admin_job_edit_notification_after_submit' ), 10, 2 );
|
add_action( 'ddhh_job_edited', array( __CLASS__, 'send_admin_job_edit_notification_after_submit' ), 10, 2 );
|
||||||
|
|
||||||
// Hook into post status transitions to detect job deactivations
|
// Hook into job deactivation to send admin notification (after metadata is saved)
|
||||||
add_action( 'transition_post_status', array( __CLASS__, 'send_admin_job_deactivation_notification' ), 10, 3 );
|
add_action( 'ddhh_job_deactivated', array( __CLASS__, 'send_admin_job_deactivation_notification' ), 10, 2 );
|
||||||
|
|
||||||
// Hook into post status transitions to notify mentors on job publish
|
// Hook into post status transitions to notify mentors on job publish
|
||||||
add_action( 'transition_post_status', array( __CLASS__, 'notify_mentors_on_job_publish' ), 10, 3 );
|
add_action( 'transition_post_status', array( __CLASS__, 'notify_mentors_on_job_publish' ), 10, 3 );
|
||||||
@@ -67,6 +67,13 @@ class DDHH_JM_Notifications {
|
|||||||
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
|
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
|
||||||
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
||||||
|
|
||||||
|
// Get job description (truncate if too long)
|
||||||
|
$job_description = $post->post_content;
|
||||||
|
$description_text = wp_strip_all_tags( $job_description );
|
||||||
|
if ( strlen( $description_text ) > 500 ) {
|
||||||
|
$description_text = substr( $description_text, 0, 500 ) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
// Format deadline if present
|
// Format deadline if present
|
||||||
$deadline_formatted = 'Nicht angegeben';
|
$deadline_formatted = 'Nicht angegeben';
|
||||||
if ( ! empty( $job_deadline ) ) {
|
if ( ! empty( $job_deadline ) ) {
|
||||||
@@ -82,36 +89,23 @@ class DDHH_JM_Notifications {
|
|||||||
// Build email subject
|
// Build email subject
|
||||||
$subject = sprintf( 'Neues Stellenangebot zur Prüfung: %s', $job_title );
|
$subject = sprintf( 'Neues Stellenangebot zur Prüfung: %s', $job_title );
|
||||||
|
|
||||||
// Build email body
|
// Build email body as HTML
|
||||||
$body = sprintf(
|
$html_body = 'Ein neues Stellenangebot wurde eingereicht und wartet auf Ihre Prüfung.<br><br>';
|
||||||
"Ein neues Stellenangebot wurde eingereicht und wartet auf Ihre Prüfung.\n\n" .
|
$html_body .= '<strong>Titel:</strong> ' . esc_html( $job_title ) . '<br>';
|
||||||
"Titel: %s\n" .
|
$html_body .= '<strong>Anbieter:</strong> ' . esc_html( $author_name ) . ' (' . esc_html( $author_org ) . ')<br>';
|
||||||
"Anbieter: %s (%s)\n" .
|
$html_body .= '<strong>Standort:</strong> ' . esc_html( $job_location ? $job_location : 'Nicht angegeben' ) . '<br>';
|
||||||
"Standort: %s\n" .
|
$html_body .= '<strong>Art:</strong> ' . esc_html( $job_type ? $job_type : 'Nicht angegeben' ) . '<br>';
|
||||||
"Art: %s\n" .
|
$html_body .= '<strong>Bewerbungsfrist:</strong> ' . esc_html( $deadline_formatted ) . '<br>';
|
||||||
"Bewerbungsfrist: %s\n" .
|
$html_body .= '<strong>Kontakt-E-Mail:</strong> ' . esc_html( $job_contact_email ? $job_contact_email : 'Nicht angegeben' ) . '<br>';
|
||||||
"Kontakt-E-Mail: %s\n" .
|
$html_body .= '<strong>Beschreibung:</strong><br>' . esc_html( $description_text ) . '<br><br>';
|
||||||
"Eingereicht am: %s\n\n" .
|
$html_body .= '<strong>Eingereicht am:</strong> ' . esc_html( $submission_date ) . '<br><br>';
|
||||||
"Prüfen Sie das Stellenangebot hier:\n%s\n\n" .
|
$html_body .= '<a href="' . esc_url( $edit_link ) . '">Stellenangebot in WordPress prüfen</a><br><br>';
|
||||||
"---\n" .
|
$html_body .= '---<br>';
|
||||||
"Diese E-Mail wurde automatisch gesendet.",
|
$html_body .= 'Diese E-Mail wurde automatisch gesendet.';
|
||||||
$job_title,
|
|
||||||
$author_name,
|
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set email headers
|
// Set email headers
|
||||||
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
||||||
|
|
||||||
// Convert plain text to HTML with line breaks
|
|
||||||
$html_body = nl2br( esc_html( $body ) );
|
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
||||||
|
|
||||||
@@ -161,6 +155,13 @@ class DDHH_JM_Notifications {
|
|||||||
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
|
$job_deadline = get_post_meta( $post->ID, 'job_deadline', true );
|
||||||
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
||||||
|
|
||||||
|
// Get job description (truncate if too long)
|
||||||
|
$job_description = $post->post_content;
|
||||||
|
$description_text = wp_strip_all_tags( $job_description );
|
||||||
|
if ( strlen( $description_text ) > 500 ) {
|
||||||
|
$description_text = substr( $description_text, 0, 500 ) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
// Format deadline if present
|
// Format deadline if present
|
||||||
$deadline_formatted = 'Nicht angegeben';
|
$deadline_formatted = 'Nicht angegeben';
|
||||||
if ( ! empty( $job_deadline ) ) {
|
if ( ! empty( $job_deadline ) ) {
|
||||||
@@ -176,36 +177,23 @@ class DDHH_JM_Notifications {
|
|||||||
// Build email subject
|
// Build email subject
|
||||||
$subject = sprintf( 'Stellenangebot bearbeitet und wartet auf Prüfung: %s', $job_title );
|
$subject = sprintf( 'Stellenangebot bearbeitet und wartet auf Prüfung: %s', $job_title );
|
||||||
|
|
||||||
// Build email body
|
// Build email body as HTML
|
||||||
$body = sprintf(
|
$html_body = 'Ein Stellenangebot wurde bearbeitet und wartet auf erneute Prüfung.<br><br>';
|
||||||
"Ein Stellenangebot wurde bearbeitet und wartet auf erneute Prüfung.\n\n" .
|
$html_body .= '<strong>Titel:</strong> ' . esc_html( $job_title ) . '<br>';
|
||||||
"Titel: %s\n" .
|
$html_body .= '<strong>Anbieter:</strong> ' . esc_html( $author_name ) . ' (' . esc_html( $author_org ) . ')<br>';
|
||||||
"Anbieter: %s (%s)\n" .
|
$html_body .= '<strong>Standort:</strong> ' . esc_html( $job_location ? $job_location : 'Nicht angegeben' ) . '<br>';
|
||||||
"Standort: %s\n" .
|
$html_body .= '<strong>Art:</strong> ' . esc_html( $job_type ? $job_type : 'Nicht angegeben' ) . '<br>';
|
||||||
"Art: %s\n" .
|
$html_body .= '<strong>Bewerbungsfrist:</strong> ' . esc_html( $deadline_formatted ) . '<br>';
|
||||||
"Bewerbungsfrist: %s\n" .
|
$html_body .= '<strong>Kontakt-E-Mail:</strong> ' . esc_html( $job_contact_email ? $job_contact_email : 'Nicht angegeben' ) . '<br>';
|
||||||
"Kontakt-E-Mail: %s\n" .
|
$html_body .= '<strong>Beschreibung:</strong><br>' . esc_html( $description_text ) . '<br><br>';
|
||||||
"Bearbeitet am: %s\n\n" .
|
$html_body .= '<strong>Bearbeitet am:</strong> ' . esc_html( $submission_date ) . '<br><br>';
|
||||||
"Prüfen Sie das Stellenangebot hier:\n%s\n\n" .
|
$html_body .= '<a href="' . esc_url( $edit_link ) . '">Stellenangebot in WordPress prüfen</a><br><br>';
|
||||||
"---\n" .
|
$html_body .= '---<br>';
|
||||||
"Diese E-Mail wurde automatisch gesendet.",
|
$html_body .= 'Diese E-Mail wurde automatisch gesendet.';
|
||||||
$job_title,
|
|
||||||
$author_name,
|
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set email headers
|
// Set email headers
|
||||||
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
||||||
|
|
||||||
// Convert plain text to HTML with line breaks
|
|
||||||
$html_body = nl2br( esc_html( $body ) );
|
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
||||||
|
|
||||||
@@ -224,19 +212,12 @@ class DDHH_JM_Notifications {
|
|||||||
/**
|
/**
|
||||||
* Send admin notification when a job is deactivated by provider
|
* Send admin notification when a job is deactivated by provider
|
||||||
*
|
*
|
||||||
* @param string $new_status New post status.
|
* @param int $post_id Post ID.
|
||||||
* @param string $old_status Old post status.
|
* @param int $entry_id Entry ID.
|
||||||
* @param WP_Post $post Post object.
|
|
||||||
*/
|
*/
|
||||||
public static function send_admin_job_deactivation_notification( $new_status, $old_status, $post ) {
|
public static function send_admin_job_deactivation_notification( $post_id, $entry_id ) {
|
||||||
// Only trigger on job_offer posts transitioning from publish to draft
|
$post = get_post( $post_id );
|
||||||
if ( 'job_offer' !== $post->post_type ) {
|
if ( ! $post || 'job_offer' !== $post->post_type ) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only send notification when published job becomes draft (deactivation)
|
|
||||||
// Avoid notification on draft saves or initial draft creation
|
|
||||||
if ( 'draft' !== $new_status || 'publish' !== $old_status ) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +249,7 @@ class DDHH_JM_Notifications {
|
|||||||
$deadline_formatted = date( 'd.m.Y', strtotime( $job_deadline ) );
|
$deadline_formatted = date( 'd.m.Y', strtotime( $job_deadline ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get deactivation reason from post meta
|
// Get deactivation reason from post meta (now saved before this hook fires)
|
||||||
$deactivation_reason = get_post_meta( $post->ID, 'job_deactivation_reason', true );
|
$deactivation_reason = get_post_meta( $post->ID, 'job_deactivation_reason', true );
|
||||||
if ( empty( $deactivation_reason ) ) {
|
if ( empty( $deactivation_reason ) ) {
|
||||||
$deactivation_reason = 'Kein Grund angegeben';
|
$deactivation_reason = 'Kein Grund angegeben';
|
||||||
@@ -283,38 +264,23 @@ class DDHH_JM_Notifications {
|
|||||||
// Build email subject
|
// Build email subject
|
||||||
$subject = sprintf( 'Stellenangebot deaktiviert: %s', $job_title );
|
$subject = sprintf( 'Stellenangebot deaktiviert: %s', $job_title );
|
||||||
|
|
||||||
// Build email body
|
// Build email body as HTML
|
||||||
$body = sprintf(
|
$html_body = 'Ein Stellenangebot wurde vom Anbieter deaktiviert.<br><br>';
|
||||||
"Ein Stellenangebot wurde vom Anbieter deaktiviert.\n\n" .
|
$html_body .= '<strong>Titel:</strong> ' . esc_html( $job_title ) . '<br>';
|
||||||
"Titel: %s\n" .
|
$html_body .= '<strong>Anbieter:</strong> ' . esc_html( $author_name ) . ' (' . esc_html( $author_org ) . ')<br>';
|
||||||
"Anbieter: %s (%s)\n" .
|
$html_body .= '<strong>Standort:</strong> ' . esc_html( $job_location ? $job_location : 'Nicht angegeben' ) . '<br>';
|
||||||
"Standort: %s\n" .
|
$html_body .= '<strong>Art:</strong> ' . esc_html( $job_type ? $job_type : 'Nicht angegeben' ) . '<br>';
|
||||||
"Art: %s\n" .
|
$html_body .= '<strong>Bewerbungsfrist:</strong> ' . esc_html( $deadline_formatted ) . '<br>';
|
||||||
"Bewerbungsfrist: %s\n" .
|
$html_body .= '<strong>Kontakt-E-Mail:</strong> ' . esc_html( $job_contact_email ? $job_contact_email : 'Nicht angegeben' ) . '<br>';
|
||||||
"Kontakt-E-Mail: %s\n" .
|
$html_body .= '<strong>Deaktiviert am:</strong> ' . esc_html( $deactivation_date ) . '<br><br>';
|
||||||
"Deaktiviert am: %s\n\n" .
|
$html_body .= '<strong>Grund für Deaktivierung:</strong><br>' . esc_html( $deactivation_reason ) . '<br><br>';
|
||||||
"Grund für Deaktivierung:\n%s\n\n" .
|
$html_body .= '<a href="' . esc_url( $edit_link ) . '">Stellenangebot in WordPress ansehen</a><br><br>';
|
||||||
"Stelle ansehen:\n%s\n\n" .
|
$html_body .= '---<br>';
|
||||||
"---\n" .
|
$html_body .= 'Diese E-Mail wurde automatisch gesendet.';
|
||||||
"Diese E-Mail wurde automatisch gesendet.",
|
|
||||||
$job_title,
|
|
||||||
$author_name,
|
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set email headers
|
// Set email headers
|
||||||
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
|
||||||
|
|
||||||
// Convert plain text to HTML with line breaks
|
|
||||||
$html_body = nl2br( esc_html( $body ) );
|
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
$sent = wp_mail( $admin_email, $subject, $html_body, $headers );
|
||||||
|
|
||||||
@@ -472,6 +438,18 @@ class DDHH_JM_Notifications {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if mentors have already been notified for this job
|
||||||
|
$already_notified = get_post_meta( $post->ID, '_ddhh_mentors_notified', true );
|
||||||
|
if ( '1' === $already_notified ) {
|
||||||
|
error_log(
|
||||||
|
sprintf(
|
||||||
|
'DDHH Job Manager: Skipping mentor notification for job #%d - mentors already notified on initial publish',
|
||||||
|
$post->ID
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get opted-in mentors
|
// Get opted-in mentors
|
||||||
$mentor_ids = DDHH_JM_User_Preferences::get_opted_in_mentors();
|
$mentor_ids = DDHH_JM_User_Preferences::get_opted_in_mentors();
|
||||||
|
|
||||||
@@ -490,6 +468,9 @@ class DDHH_JM_Notifications {
|
|||||||
// Schedule async batch notifications
|
// Schedule async batch notifications
|
||||||
$batch_count = DDHH_JM_Scheduler::schedule_mentor_notification_batch( $mentor_ids, $post->ID );
|
$batch_count = DDHH_JM_Scheduler::schedule_mentor_notification_batch( $mentor_ids, $post->ID );
|
||||||
|
|
||||||
|
// Mark job as having notified mentors to prevent duplicate notifications
|
||||||
|
update_post_meta( $post->ID, '_ddhh_mentors_notified', '1' );
|
||||||
|
|
||||||
// Log success
|
// Log success
|
||||||
error_log(
|
error_log(
|
||||||
sprintf(
|
sprintf(
|
||||||
|
|||||||
@@ -15,6 +15,40 @@ defined( 'ABSPATH' ) || exit;
|
|||||||
*/
|
*/
|
||||||
class DDHH_JM_Pages {
|
class DDHH_JM_Pages {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup hooks
|
||||||
|
*/
|
||||||
|
public static function setup_hooks() {
|
||||||
|
// Redirect logged-in providers from login page to dashboard
|
||||||
|
add_action( 'template_redirect', array( __CLASS__, 'maybe_redirect_logged_in_from_login' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect logged-in providers from login page to dashboard
|
||||||
|
*/
|
||||||
|
public static function maybe_redirect_logged_in_from_login() {
|
||||||
|
// Only check on the login page
|
||||||
|
$login_page_id = get_option( 'ddhh_jm_login_page_id' );
|
||||||
|
if ( ! $login_page_id || ! is_page( $login_page_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user is logged in and is a provider, redirect to dashboard
|
||||||
|
if ( is_user_logged_in() ) {
|
||||||
|
$user = wp_get_current_user();
|
||||||
|
if ( in_array( 'ddhh_provider', $user->roles, true ) ) {
|
||||||
|
$dashboard_page_id = get_option( 'ddhh_jm_dashboard_page_id' );
|
||||||
|
if ( $dashboard_page_id ) {
|
||||||
|
$dashboard_url = get_permalink( $dashboard_page_id );
|
||||||
|
if ( $dashboard_url ) {
|
||||||
|
wp_safe_redirect( $dashboard_url );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create provider pages on plugin activation
|
* Create provider pages on plugin activation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ class DDHH_JM_Template {
|
|||||||
$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_deadline = get_post_meta( $post->ID, 'job_deadline', true );
|
||||||
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
$job_contact_email = get_post_meta( $post->ID, 'job_contact_email', true );
|
||||||
$job_logo = get_the_post_thumbnail( $post->ID, 'job-logo' );
|
$provider_logo_id = get_user_meta( $post->post_author, 'ddhh_provider_logo', true );
|
||||||
|
$job_logo = $provider_logo_id ? wp_get_attachment_image( absint( $provider_logo_id ), 'job-logo' ) : '';
|
||||||
|
|
||||||
// Get author/organization info
|
// Get author/organization info
|
||||||
$author = get_userdata( $post->post_author );
|
$author = get_userdata( $post->post_author );
|
||||||
@@ -50,6 +51,9 @@ class DDHH_JM_Template {
|
|||||||
// Build job details HTML
|
// Build job details HTML
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
|
<div class="ddhh-back-to-archive">
|
||||||
|
<a href="<?php echo esc_url( get_post_type_archive_link( 'job_offer' ) ); ?>">← Alle Jobangebote</a>
|
||||||
|
</div>
|
||||||
<div class="ddhh-job-offer-details">
|
<div class="ddhh-job-offer-details">
|
||||||
<?php if ( $job_logo ) : ?>
|
<?php if ( $job_logo ) : ?>
|
||||||
<div class="job-logo">
|
<div class="job-logo">
|
||||||
@@ -118,6 +122,18 @@ class DDHH_JM_Template {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.ddhh-back-to-archive {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.ddhh-back-to-archive a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.ddhh-back-to-archive a:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
.ddhh-job-offer-details {
|
.ddhh-job-offer-details {
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
}
|
}
|
||||||
|
|||||||
38
includes/elementor-tags/class-tag-job-contact-email.php
Normal file
38
includes/elementor-tags/class-tag-job-contact-email.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor dynamic tag: Kontakt-E-Mail (job_contact_email).
|
||||||
|
*
|
||||||
|
* Registered in both TEXT and URL categories so it can be used
|
||||||
|
* as link href (mailto:) or plain text display.
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
class DDHH_JM_Tag_Job_Contact_Email extends \Elementor\Core\DynamicTags\Tag {
|
||||||
|
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'ddhh-job-contact-email';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title(): string {
|
||||||
|
return 'Kontakt-E-Mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_group(): array {
|
||||||
|
return array( DDHH_JM_Elementor_Tags::GROUP_SLUG );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories(): array {
|
||||||
|
return array(
|
||||||
|
\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY,
|
||||||
|
\Elementor\Modules\DynamicTags\Module::URL_CATEGORY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): void {
|
||||||
|
$value = get_post_meta( get_the_ID(), 'job_contact_email', true );
|
||||||
|
echo esc_html( $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
44
includes/elementor-tags/class-tag-job-deadline.php
Normal file
44
includes/elementor-tags/class-tag-job-deadline.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor dynamic tag: Bewerbungsfrist (job_deadline).
|
||||||
|
*
|
||||||
|
* Formats the stored Y-m-d date as DD.MM.YYYY for display.
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
class DDHH_JM_Tag_Job_Deadline extends \Elementor\Core\DynamicTags\Tag {
|
||||||
|
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'ddhh-job-deadline';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title(): string {
|
||||||
|
return 'Bewerbungsfrist';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_group(): array {
|
||||||
|
return array( DDHH_JM_Elementor_Tags::GROUP_SLUG );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories(): array {
|
||||||
|
return array( \Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): void {
|
||||||
|
$raw = get_post_meta( get_the_ID(), 'job_deadline', true );
|
||||||
|
if ( empty( $raw ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = strtotime( $raw );
|
||||||
|
if ( false === $timestamp ) {
|
||||||
|
echo esc_html( $raw );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo esc_html( date_i18n( 'd.m.Y', $timestamp ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
32
includes/elementor-tags/class-tag-job-location.php
Normal file
32
includes/elementor-tags/class-tag-job-location.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor dynamic tag: Standort (job_location).
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
class DDHH_JM_Tag_Job_Location extends \Elementor\Core\DynamicTags\Tag {
|
||||||
|
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'ddhh-job-location';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title(): string {
|
||||||
|
return 'Standort';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_group(): array {
|
||||||
|
return array( DDHH_JM_Elementor_Tags::GROUP_SLUG );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories(): array {
|
||||||
|
return array( \Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): void {
|
||||||
|
$value = get_post_meta( get_the_ID(), 'job_location', true );
|
||||||
|
echo wp_kses_post( $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
68
includes/elementor-tags/class-tag-job-logo.php
Normal file
68
includes/elementor-tags/class-tag-job-logo.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor dynamic tag: Logo.
|
||||||
|
*
|
||||||
|
* Extends Data_Tag to return image data (id + url) for use in
|
||||||
|
* Image widgets and other image-accepting controls.
|
||||||
|
*
|
||||||
|
* The logo is stored on the provider (post author) as user meta
|
||||||
|
* `ddhh_provider_logo`, not on the job post itself.
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
class DDHH_JM_Tag_Job_Logo extends \Elementor\Core\DynamicTags\Data_Tag {
|
||||||
|
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'ddhh-job-logo';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title(): string {
|
||||||
|
return 'Logo';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_group(): array {
|
||||||
|
return array( DDHH_JM_Elementor_Tags::GROUP_SLUG );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories(): array {
|
||||||
|
return array( \Elementor\Modules\DynamicTags\Module::IMAGE_CATEGORY );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function register_controls(): void {
|
||||||
|
$this->add_control(
|
||||||
|
'fallback',
|
||||||
|
array(
|
||||||
|
'label' => 'Fallback',
|
||||||
|
'type' => \Elementor\Controls_Manager::MEDIA,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_value( array $options = array() ): array {
|
||||||
|
$post = get_post( get_the_ID() );
|
||||||
|
$image_id = $post ? get_user_meta( $post->post_author, 'ddhh_provider_logo', true ) : '';
|
||||||
|
|
||||||
|
if ( $image_id ) {
|
||||||
|
$url = wp_get_attachment_image_url( $image_id, 'full' );
|
||||||
|
if ( $url ) {
|
||||||
|
return array(
|
||||||
|
'id' => (int) $image_id,
|
||||||
|
'url' => $url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = $this->get_settings( 'fallback' );
|
||||||
|
if ( ! empty( $fallback['id'] ) ) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => 0,
|
||||||
|
'url' => '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
includes/elementor-tags/class-tag-job-type.php
Normal file
41
includes/elementor-tags/class-tag-job-type.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Elementor dynamic tag: Art (job_type).
|
||||||
|
*
|
||||||
|
* Maps raw select values to German display labels.
|
||||||
|
*
|
||||||
|
* @package DDHH_Job_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
class DDHH_JM_Tag_Job_Type extends \Elementor\Core\DynamicTags\Tag {
|
||||||
|
|
||||||
|
private const LABELS = array(
|
||||||
|
'vollzeit' => 'Vollzeit',
|
||||||
|
'teilzeit' => 'Teilzeit',
|
||||||
|
'ehrenamt' => 'Ehrenamt',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'ddhh-job-type';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title(): string {
|
||||||
|
return 'Art';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_group(): array {
|
||||||
|
return array( DDHH_JM_Elementor_Tags::GROUP_SLUG );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories(): array {
|
||||||
|
return array( \Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): void {
|
||||||
|
$raw = get_post_meta( get_the_ID(), 'job_type', true );
|
||||||
|
$label = self::LABELS[ $raw ] ?? $raw;
|
||||||
|
echo esc_html( $label );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,12 +26,58 @@ if ( ! in_array( 'ddhh_provider', $current_user->roles, true ) ) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle logo upload
|
||||||
|
if ( isset( $_POST['ddhh_upload_logo'] ) && isset( $_POST['ddhh_logo_nonce'] ) && wp_verify_nonce( $_POST['ddhh_logo_nonce'], 'ddhh_logo_upload' ) ) {
|
||||||
|
if ( ! empty( $_FILES['ddhh_logo_file'] ) && $_FILES['ddhh_logo_file']['error'] === UPLOAD_ERR_OK ) {
|
||||||
|
require_once( ABSPATH . 'wp-admin/includes/file.php' );
|
||||||
|
require_once( ABSPATH . 'wp-admin/includes/image.php' );
|
||||||
|
require_once( ABSPATH . 'wp-admin/includes/media.php' );
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
$allowed_types = array( 'image/jpeg', 'image/png', 'image/jpg' );
|
||||||
|
$file_type = $_FILES['ddhh_logo_file']['type'];
|
||||||
|
|
||||||
|
if ( in_array( $file_type, $allowed_types, true ) ) {
|
||||||
|
$upload = wp_handle_upload( $_FILES['ddhh_logo_file'], array( 'test_form' => false ) );
|
||||||
|
|
||||||
|
if ( isset( $upload['file'] ) && ! isset( $upload['error'] ) ) {
|
||||||
|
// Insert attachment
|
||||||
|
$attachment = array(
|
||||||
|
'post_mime_type' => $upload['type'],
|
||||||
|
'post_title' => sanitize_file_name( $_FILES['ddhh_logo_file']['name'] ),
|
||||||
|
'post_content' => '',
|
||||||
|
'post_status' => 'inherit',
|
||||||
|
);
|
||||||
|
|
||||||
|
$attachment_id = wp_insert_attachment( $attachment, $upload['file'] );
|
||||||
|
|
||||||
|
if ( ! is_wp_error( $attachment_id ) ) {
|
||||||
|
// Generate metadata
|
||||||
|
$attachment_data = wp_generate_attachment_metadata( $attachment_id, $upload['file'] );
|
||||||
|
wp_update_attachment_metadata( $attachment_id, $attachment_data );
|
||||||
|
|
||||||
|
// Save to user meta
|
||||||
|
update_user_meta( get_current_user_id(), 'ddhh_provider_logo', $attachment_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logo removal
|
||||||
|
if ( isset( $_POST['ddhh_remove_logo'] ) && isset( $_POST['ddhh_logo_nonce'] ) && wp_verify_nonce( $_POST['ddhh_logo_nonce'], 'ddhh_logo_upload' ) ) {
|
||||||
|
delete_user_meta( get_current_user_id(), 'ddhh_provider_logo' );
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're in edit mode
|
// Check if we're in edit mode
|
||||||
$is_edit_mode = isset( $_GET['action'] ) && $_GET['action'] === 'edit_job' && isset( $_GET['job_id'] );
|
$is_edit_mode = isset( $_GET['action'] ) && $_GET['action'] === 'edit_job' && isset( $_GET['job_id'] );
|
||||||
|
|
||||||
// Check if we're in deactivate mode
|
// Check if we're in deactivate mode
|
||||||
$is_deactivate_mode = isset( $_GET['action'] ) && $_GET['action'] === 'deactivate_job' && isset( $_GET['job_id'] );
|
$is_deactivate_mode = isset( $_GET['action'] ) && $_GET['action'] === 'deactivate_job' && isset( $_GET['job_id'] );
|
||||||
|
|
||||||
|
// Check if we're in new job mode
|
||||||
|
$is_new_job_mode = isset( $_GET['action'] ) && $_GET['action'] === 'new_job';
|
||||||
|
|
||||||
if ( $is_edit_mode ) {
|
if ( $is_edit_mode ) {
|
||||||
$job_id = absint( $_GET['job_id'] );
|
$job_id = absint( $_GET['job_id'] );
|
||||||
$form_id = DDHH_JM_Formidable::get_job_edit_form_id();
|
$form_id = DDHH_JM_Formidable::get_job_edit_form_id();
|
||||||
@@ -69,9 +115,6 @@ if ( $is_edit_mode ) {
|
|||||||
case 'job_contact_email2':
|
case 'job_contact_email2':
|
||||||
$field_value = get_post_meta( $job_id, 'job_contact_email', true );
|
$field_value = get_post_meta( $job_id, 'job_contact_email', true );
|
||||||
break;
|
break;
|
||||||
case 'job_logo2':
|
|
||||||
$field_value = get_post_thumbnail_id( $job_id );
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $field_value ) ) {
|
if ( ! empty( $field_value ) ) {
|
||||||
@@ -81,6 +124,15 @@ if ( $is_edit_mode ) {
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="ddhh-provider-dashboard">
|
<div class="ddhh-provider-dashboard">
|
||||||
|
<div class="ddhh-dashboard-header">
|
||||||
|
<div class="ddhh-user-info">
|
||||||
|
<span class="welcome-text">Angemeldet als: <strong><?php echo esc_html( $current_user->display_name ); ?></strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="ddhh-logout-link">
|
||||||
|
<a href="<?php echo esc_url( wp_logout_url( home_url( '/anbieter-login/' ) ) ); ?>" class="logout-button">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ddhh-job-edit-section">
|
<div class="ddhh-job-edit-section">
|
||||||
<h2>Stellenangebot bearbeiten</h2>
|
<h2>Stellenangebot bearbeiten</h2>
|
||||||
<p><a href="<?php echo esc_url( home_url( '/anbieter-dashboard/' ) ); ?>" 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>
|
||||||
@@ -185,6 +237,15 @@ if ( $is_deactivate_mode ) {
|
|||||||
if ( $form_id ) {
|
if ( $form_id ) {
|
||||||
?>
|
?>
|
||||||
<div class="ddhh-provider-dashboard">
|
<div class="ddhh-provider-dashboard">
|
||||||
|
<div class="ddhh-dashboard-header">
|
||||||
|
<div class="ddhh-user-info">
|
||||||
|
<span class="welcome-text">Angemeldet als: <strong><?php echo esc_html( $current_user->display_name ); ?></strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="ddhh-logout-link">
|
||||||
|
<a href="<?php echo esc_url( wp_logout_url( home_url( '/anbieter-login/' ) ) ); ?>" class="logout-button">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ddhh-job-deactivate-section">
|
<div class="ddhh-job-deactivate-section">
|
||||||
<h2>Stellenangebot deaktivieren</h2>
|
<h2>Stellenangebot deaktivieren</h2>
|
||||||
<p><a href="<?php echo esc_url( home_url( '/anbieter-dashboard/' ) ); ?>" 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>
|
||||||
@@ -196,6 +257,32 @@ if ( $is_deactivate_mode ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( $is_new_job_mode ) {
|
||||||
|
$form_id = DDHH_JM_Formidable::get_job_submission_form_id();
|
||||||
|
|
||||||
|
if ( $form_id ) {
|
||||||
|
?>
|
||||||
|
<div class="ddhh-provider-dashboard">
|
||||||
|
<div class="ddhh-dashboard-header">
|
||||||
|
<div class="ddhh-user-info">
|
||||||
|
<span class="welcome-text">Angemeldet als: <strong><?php echo esc_html( $current_user->display_name ); ?></strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="ddhh-logout-link">
|
||||||
|
<a href="<?php echo esc_url( wp_logout_url( home_url( '/anbieter-login/' ) ) ); ?>" class="logout-button">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ddhh-job-submit-section">
|
||||||
|
<h2>Neues Stellenangebot erstellen</h2>
|
||||||
|
<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}]" ); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Query current user's job_offer posts
|
// Query current user's job_offer posts
|
||||||
$args = array(
|
$args = array(
|
||||||
'post_type' => 'job_offer',
|
'post_type' => 'job_offer',
|
||||||
@@ -210,16 +297,42 @@ $job_query = new WP_Query( $args );
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="ddhh-provider-dashboard">
|
<div class="ddhh-provider-dashboard">
|
||||||
<div class="ddhh-job-submit-section">
|
<div class="ddhh-dashboard-header">
|
||||||
<h2>Neues Stellenangebot erstellen</h2>
|
<div class="ddhh-user-info">
|
||||||
|
<span class="welcome-text">Angemeldet als: <strong><?php echo esc_html( $current_user->display_name ); ?></strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="ddhh-logout-link">
|
||||||
|
<a href="<?php echo esc_url( wp_logout_url( home_url( '/anbieter-login/' ) ) ); ?>" class="logout-button">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ddhh-logo-section">
|
||||||
|
<h3>Ihr Logo</h3>
|
||||||
<?php
|
<?php
|
||||||
$form_id = DDHH_JM_Formidable::get_job_submission_form_id();
|
$provider_logo_id = get_user_meta( get_current_user_id(), 'ddhh_provider_logo', true );
|
||||||
if ( $form_id ) {
|
if ( $provider_logo_id ) {
|
||||||
echo do_shortcode( "[formidable id={$form_id}]" );
|
echo '<div class="ddhh-current-logo">';
|
||||||
} else {
|
echo wp_get_attachment_image( absint( $provider_logo_id ), 'medium' );
|
||||||
echo '<p>Formular konnte nicht geladen werden.</p>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
<form method="post" enctype="multipart/form-data" class="ddhh-logo-upload-form">
|
||||||
|
<?php wp_nonce_field( 'ddhh_logo_upload', 'ddhh_logo_nonce' ); ?>
|
||||||
|
<div class="ddhh-logo-controls">
|
||||||
|
<input type="file" name="ddhh_logo_file" accept="image/png,image/jpeg,image/jpg" class="ddhh-logo-input">
|
||||||
|
<button type="submit" name="ddhh_upload_logo" class="ddhh-logo-button ddhh-upload-button">
|
||||||
|
<?php echo $provider_logo_id ? 'Logo ändern' : 'Logo hochladen'; ?>
|
||||||
|
</button>
|
||||||
|
<?php if ( $provider_logo_id ) : ?>
|
||||||
|
<button type="submit" name="ddhh_remove_logo" class="ddhh-logo-button ddhh-remove-button" onclick="return confirm('Möchten Sie das Logo wirklich entfernen?');">Logo entfernen</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<p class="ddhh-logo-hint">Erlaubte Formate: JPG, PNG (max. 2 MB)</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ddhh-dashboard-actions">
|
||||||
|
<a href="<?php echo esc_url( add_query_arg( 'action', 'new_job', home_url( '/anbieter-dashboard/' ) ) ); ?>" class="ddhh-new-job-button">+ Neues Stellenangebot erstellen</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ddhh-job-listings-section">
|
<div class="ddhh-job-listings-section">
|
||||||
@@ -314,19 +427,166 @@ $job_query = new WP_Query( $args );
|
|||||||
.ddhh-provider-dashboard {
|
.ddhh-provider-dashboard {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 0 1rem;
|
padding: 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.25rem 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-user-info .welcome-text {
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-user-info strong {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-dashboard-header .logout-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
background-color: #6b7280;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-dashboard-header .logout-button:hover {
|
||||||
|
background-color: #4b5563;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-section {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-current-logo {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-current-logo img {
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-upload-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-upload-button {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-upload-button:hover {
|
||||||
|
background-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-remove-button {
|
||||||
|
background-color: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-remove-button:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logo-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-dashboard-actions {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-provider-dashboard .ddhh-new-job-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.875rem 2rem;
|
||||||
|
background-color: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-provider-dashboard .ddhh-new-job-button:hover {
|
||||||
|
background-color: #059669;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ddhh-job-submit-section {
|
.ddhh-job-submit-section {
|
||||||
margin-bottom: 3rem;
|
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background: #f5f5f5;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ddhh-job-submit-section h2 {
|
.ddhh-job-submit-section h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
@@ -424,7 +684,7 @@ $job_query = new WP_Query( $args );
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.ddhh-jobs-table .button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background-color: #3b82f6;
|
background-color: #3b82f6;
|
||||||
@@ -435,35 +695,42 @@ $job_query = new WP_Query( $args );
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.ddhh-jobs-table .button:hover {
|
||||||
background-color: #2563eb;
|
background-color: #2563eb;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-jobs-table .edit-link {
|
||||||
|
background-color: #6366f1;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-link {
|
.ddhh-jobs-table .edit-link:hover {
|
||||||
background-color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-link:hover {
|
|
||||||
background-color: #4f46e5;
|
background-color: #4f46e5;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-link {
|
.ddhh-jobs-table .view-link {
|
||||||
background-color: #10b981;
|
background-color: #10b981;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-link:hover {
|
.ddhh-jobs-table .view-link:hover {
|
||||||
background-color: #059669;
|
background-color: #059669;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deactivate-link {
|
.ddhh-jobs-table .deactivate-link {
|
||||||
background-color: #ef4444;
|
background-color: #ef4444;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deactivate-link:hover {
|
.ddhh-jobs-table .deactivate-link:hover {
|
||||||
background-color: #dc2626;
|
background-color: #dc2626;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ddhh-empty-state,
|
.ddhh-empty-state,
|
||||||
@@ -481,6 +748,22 @@ $job_query = new WP_Query( $args );
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.ddhh-dashboard-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ddhh-logout-link {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.ddhh-jobs-table {
|
.ddhh-jobs-table {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user