diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca9737..a596d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.3.1] +### Changed +- The interface now says **student** where it said "child" and **profile** where it said "family". The `[us_family]` page is headed **Your profile**, its form is **Add a student**, signup asks for a **Student's name**, and the wp-admin students list and student screen both label the relationship **Profile**. Two strings were reworded rather than swapped: the students list reads **Managed by _name_** (a bare "Student of _name_" would read as a teacher's pupil), and a managed account is described as a **managed student account** so it is not confused with the account holder. Internal names — database columns, request parameters, form field names, the `us_family` shortcode and the `us-scheduler/family` block — are unchanged, since they are contracts with existing installs and saved post content. + ### Fixed - Upcoming lesson rows no longer render on top of each other. The row's text sits in inline elements that a theme can pull out of normal flow, which dropped the date and time onto the lesson title and the status pill onto the Cancel button; those elements are now pinned into flow alongside the rest of the panel's theme-proofing. The rows held behind **Show all** also stayed visible under the `div { display: block }` reset that many themes still carry, since `[hidden]` is only a browser default — they are now hidden for real. diff --git a/assets/js/blocks.js b/assets/js/blocks.js index 2ce1ff0..d643f28 100644 --- a/assets/js/blocks.js +++ b/assets/js/blocks.js @@ -284,10 +284,13 @@ }, { name: 'us-scheduler/family', - title: __('Family', 'unsupervised-schedular'), - description: __('Lets a parent or guardian add, edit and remove the children they book lessons for.', 'unsupervised-schedular'), + title: __('Profile', 'unsupervised-schedular'), + description: __('Lets a parent or guardian add, edit and remove the students they book lessons for.', 'unsupervised-schedular'), icon: 'groups', - keywords: ['family', 'children', 'guardian', 'parent'], + // 'family' and 'children' are kept as search terms only — they are + // never displayed, and the block answered to them before it was + // renamed, so anyone reaching for the old word still finds it. + keywords: ['profile', 'students', 'family', 'children', 'guardian', 'parent'], shortcode: 'us_family', attributes: { loginPageId: { type: 'number', default: 0 }, diff --git a/docs/features/parent-guardian-accounts.md b/docs/features/parent-guardian-accounts.md index 1d6baf8..ed0cbe2 100644 --- a/docs/features/parent-guardian-accounts.md +++ b/docs/features/parent-guardian-accounts.md @@ -9,6 +9,19 @@ A guardian may also be a student in their own right — they appear in their own "who is this for?" selector alongside their children, so a parent taking lessons next to their kids needs only the one account. +## Vocabulary: "child" in the code, "student" in the UI + +The interface says **student** and **profile**; the code says **child** and +**family**. This is deliberate, not drift. Every identifier below — the +`us_guardian_links` columns, `GuardianService::createChild()`, the `children[]` +request parameters, the `child_name` form fields, the `us-scheduler/family` +block name and the `[us_family]` shortcode — is a stable contract with the +database, saved post content and existing installs, so renaming them would break +sites for no user-visible gain. Only the strings a person reads were changed. + +When adding to this feature, keep the split: internal names follow the +data model, translatable strings follow the interface. + ## Core Decision: children are accountless WordPress users Every `student_id` column in `src/Schema.php` (`us_lessons`, `us_payments`, @@ -159,7 +172,7 @@ child. ## Managing children -`[us_family]` (block: **Family**) renders the guardian's manage-children screen: +`[us_family]` (block: **Profile**) renders the guardian's manage-children screen: list the children, add one, edit a name/date of birth, remove one. - **Add** creates another accountless child user and links it. Account-scope @@ -219,11 +232,11 @@ than being left as a single-student-only path. ## Admin -- **Students list** gains a **Guardian / Children** column: a child links to its +- **Students list** gains a **Profile** column: a child links to its guardian's detail screen, a guardian lists its children as links. Children are listed alongside every other student rather than nested, so nothing about finding a student changes. -- **Student detail** gains a **Family** panel — the guardian (for a child) or +- **Student detail** gains a **Profile** panel — the guardian (for a child) or the children (for a guardian), each a link to the other's screen — and the credit balance shown is the **payer's** balance, labelled with whose it is, so an admin looking at a child sees the family balance that will actually settle diff --git a/docs/features/student-administration.md b/docs/features/student-administration.md index 95c7d74..4a0abfc 100644 --- a/docs/features/student-administration.md +++ b/docs/features/student-administration.md @@ -88,8 +88,8 @@ All actions are nonce-protected POSTs handled on the detail page: `tests/Unit/Payment/PaymentRepositoryTest.php` ## Family Relationships -The students list gains a **Family** column — a child links to their guardian, -a guardian lists their children — and the student screen a **Family** panel. A +The students list gains a **Profile** column — a child links to their guardian, +a guardian lists their children — and the student screen a **Profile** panel. A child's listed email is their guardian's, since a child's own address is an undeliverable placeholder, and the credit balance shown is the payer's, labelled with whose account holds it. See `parent-guardian-accounts.md`. diff --git a/src/Auth/RegistrationPage.php b/src/Auth/RegistrationPage.php index d2f3c6c..5f1e85a 100644 --- a/src/Auth/RegistrationPage.php +++ b/src/Auth/RegistrationPage.php @@ -286,14 +286,14 @@ class RegistrationPage { // Everything is validated before a single user is created, so a bad child // block never leaves a half-registered family behind. if ( $isGuardian && [] === $children ) { - return esc_html__( 'Please add at least one child, or uncheck the parent/guardian option.', 'unsupervised-schedular' ); + return esc_html__( 'Please add at least one student, or uncheck the parent/guardian option.', 'unsupervised-schedular' ); } foreach ( $isGuardian ? array_column( $children, 'answers' ) : [ $answers ] as $set ) { foreach ( $accountQuestions as $question ) { if ( $question->isRequired && '' === trim( (string) ( $set[ (int) $question->id ] ?? '' ) ) ) { return $isGuardian - ? esc_html__( 'Please answer all required registration questions for each child.', 'unsupervised-schedular' ) + ? esc_html__( 'Please answer all required registration questions for each student.', 'unsupervised-schedular' ) : esc_html__( 'Please answer all required registration questions.', 'unsupervised-schedular' ); } } diff --git a/src/BlockPreview.php b/src/BlockPreview.php index cbe11cc..2457500 100644 --- a/src/BlockPreview.php +++ b/src/BlockPreview.php @@ -189,16 +189,16 @@ class BlockPreview { '

%s

' . '

' . '

', - esc_html__( 'Add a child', 'unsupervised-schedular' ), + esc_html__( 'Add a student', 'unsupervised-schedular' ), esc_html__( 'Name', 'unsupervised-schedular' ), esc_html__( 'Date of birth', 'unsupervised-schedular' ), - esc_html__( 'Add child', 'unsupervised-schedular' ) + esc_html__( 'Add student', 'unsupervised-schedular' ) ); return sprintf( '
%s

%s

%s
', - self::note( __( 'Editor preview — signed-in guardians see and manage their own children here.', 'unsupervised-schedular' ) ), - esc_html__( 'Your family', 'unsupervised-schedular' ), + self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ), + esc_html__( 'Your profile', 'unsupervised-schedular' ), $children, $add ); diff --git a/src/Guardian/ChildLoginGate.php b/src/Guardian/ChildLoginGate.php index 58d3509..e60bc8c 100644 --- a/src/Guardian/ChildLoginGate.php +++ b/src/Guardian/ChildLoginGate.php @@ -35,7 +35,7 @@ class ChildLoginGate { if ( $user instanceof \WP_User && GuardianService::isChild( (int) $user->ID ) ) { return new \WP_Error( 'us_child_account', - esc_html__( 'This is a child account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' ) + esc_html__( 'This is a managed student account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' ) ); } diff --git a/src/Guardian/FamilyPage.php b/src/Guardian/FamilyPage.php index e93543f..9805662 100644 --- a/src/Guardian/FamilyPage.php +++ b/src/Guardian/FamilyPage.php @@ -50,7 +50,7 @@ class FamilyPage { '

%s %s.

', esc_html__( 'Please', 'unsupervised-schedular' ), esc_url( $this->loginUrl( $loginPageId ) ), - esc_html__( 'log in to manage your family', 'unsupervised-schedular' ) + esc_html__( 'log in to manage your profile', 'unsupervised-schedular' ) ); } @@ -168,7 +168,7 @@ class FamilyPage { private function firstMissingAnswer( array $questions, array $answers ): ?\WP_Error { foreach ( $questions as $question ) { if ( $question->isRequired && '' === trim( (string) ( $answers[ (int) $question->id ] ?? '' ) ) ) { - return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this child.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this student.', 'unsupervised-schedular' ) ); } } @@ -237,9 +237,9 @@ class FamilyPage { */ private function noticeFor( string $result ): string { return match ( $result ) { - self::RESULT_ADDED => __( 'Child added.', 'unsupervised-schedular' ), + self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ), self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ), - self::RESULT_REMOVED => __( 'Child removed.', 'unsupervised-schedular' ), + self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ), default => '', }; } diff --git a/src/Guardian/GuardianService.php b/src/Guardian/GuardianService.php index 315d7f0..3ff8282 100644 --- a/src/Guardian/GuardianService.php +++ b/src/Guardian/GuardianService.php @@ -51,7 +51,7 @@ class GuardianService { public function createChild( int $guardianId, string $name, string $dateOfBirth = '', string $relationship = '' ): int|\WP_Error { $name = trim( $name ); if ( '' === $name ) { - return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) ); } $email = $this->childEmail(); @@ -89,7 +89,7 @@ class GuardianService { if ( $linkId <= 0 ) { $this->deleteUser( $userId ); - return new \WP_Error( 'link_failed', __( 'Could not add this child. Please contact the studio.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'link_failed', __( 'Could not add this student. Please contact the studio.', 'unsupervised-schedular' ) ); } return $userId; @@ -104,12 +104,12 @@ class GuardianService { */ public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): ?\WP_Error { if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) { - return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) ); } $name = trim( $name ); if ( '' === $name ) { - return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) ); } $result = wp_update_user( @@ -139,13 +139,13 @@ class GuardianService { */ public function removeChild( int $guardianId, int $studentId ): ?\WP_Error { if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) { - return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) ); + return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) ); } if ( [] !== $this->bookings->findByStudent( $studentId ) || [] !== $this->enrollments->findByStudent( $studentId ) ) { return new \WP_Error( 'has_history', - __( 'This child has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' ) + __( 'This student has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' ) ); } diff --git a/templates/admin/student-detail.php b/templates/admin/student-detail.php index dd65952..94b799e 100644 --- a/templates/admin/student-detail.php +++ b/templates/admin/student-detail.php @@ -110,7 +110,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void -

+

add_query_arg(['page' => $pageSlug, 'student_id' => $id], admin_url('admin.php')); ?>

@@ -123,7 +123,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void ); ?>

-

+

@@ -293,7 +293,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void diff --git a/templates/admin/students.php b/templates/admin/students.php index c9e4cfe..b2ddd81 100644 --- a/templates/admin/students.php +++ b/templates/admin/students.php @@ -25,7 +25,7 @@ $familyCell = static function (array $student) use ($pageSlug): string { if ($student['guardian'] !== null) { return sprintf( /* translators: %s: linked name of the parent/guardian who books for this student. */ - esc_html__('Child of %s', 'unsupervised-schedular'), + esc_html__('Managed by %s', 'unsupervised-schedular'), $link($student['guardian']['id'], $student['guardian']['name']) ); } @@ -51,7 +51,7 @@ $familyCell = static function (array $student) use ($pageSlug): string { - + diff --git a/templates/frontend/family-page.php b/templates/frontend/family-page.php index b0658f8..466ed63 100644 --- a/templates/frontend/family-page.php +++ b/templates/frontend/family-page.php @@ -16,7 +16,7 @@ if (! defined('ABSPATH')) { */ ?>
-

+

@@ -27,7 +27,7 @@ if (! defined('ABSPATH')) { -

+

diff --git a/templates/frontend/register-page.php b/templates/frontend/register-page.php index 2db52c0..6a756e2 100644 --- a/templates/frontend/register-page.php +++ b/templates/frontend/register-page.php @@ -75,18 +75,18 @@ if (! defined('ABSPATH')) {

-

+

- +

- +

@@ -107,7 +107,7 @@ if (! defined('ABSPATH')) {

- +

diff --git a/tests/Unit/Auth/RegistrationPageTest.php b/tests/Unit/Auth/RegistrationPageTest.php index 1d1b1a8..e0b1ecd 100644 --- a/tests/Unit/Auth/RegistrationPageTest.php +++ b/tests/Unit/Auth/RegistrationPageTest.php @@ -667,7 +667,7 @@ class RegistrationPageTest extends TestCase $result = $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false); - self::assertStringContainsString('at least one child', $result); + self::assertStringContainsString('at least one student', $result); } /** @@ -694,7 +694,7 @@ class RegistrationPageTest extends TestCase Functions\expect('wp_insert_user')->never(); $this->ctx['guardians']->shouldNotReceive('createChild'); - self::assertStringContainsString('for each child', $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false)); + self::assertStringContainsString('for each student', $this->submit(new Invite(email: 'a@b.test', token: 'hash'), false)); } /** diff --git a/tests/Unit/Guardian/FamilyPageTest.php b/tests/Unit/Guardian/FamilyPageTest.php index b7f183b..3730484 100644 --- a/tests/Unit/Guardian/FamilyPageTest.php +++ b/tests/Unit/Guardian/FamilyPageTest.php @@ -86,7 +86,7 @@ class FamilyPageTest extends TestCase $html = $this->page->render([]); - self::assertStringContainsString('log in to manage your family', $html); + self::assertStringContainsString('log in to manage your profile', $html); } public function testRenderListsTheGuardiansChildren(): void @@ -100,7 +100,7 @@ class FamilyPageTest extends TestCase self::assertStringContainsString('Ada', $html); self::assertStringContainsString('2015-04-02', $html); - self::assertStringContainsString('Add a child', $html); + self::assertStringContainsString('Add a student', $html); } public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void @@ -161,7 +161,7 @@ class FamilyPageTest extends TestCase $_POST = ['us_family_action' => 'add', 'child_name' => '']; $this->questions->shouldReceive('findByScope')->once()->andReturn([]); - $this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each child a name.')); + $this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each student a name.')); $this->answers->shouldNotReceive('insert'); $captured = null; @@ -207,7 +207,7 @@ class FamilyPageTest extends TestCase $_POST = ['us_family_action' => 'remove', 'child_id' => '42']; $this->guardians->shouldReceive('removeChild')->once()->andReturn( - new \WP_Error('has_history', 'This child has lessons or enrolments on record.') + new \WP_Error('has_history', 'This student has lessons or enrolments on record.') ); $captured = null; @@ -271,6 +271,6 @@ class FamilyPageTest extends TestCase $this->guardians->shouldReceive('children')->andReturn([]); $this->questions->shouldReceive('findByScope')->andReturn([]); - self::assertStringContainsString('Child added.', $this->page->render([])); + self::assertStringContainsString('Student added.', $this->page->render([])); } }