1. 30 Aug, 2026 13 commits
    • Claude's avatar
      Itemise financial expenses and stop double-counting refunds · 99c1d2b6
      Claude authored
      Three separate defects made the financial figures wrong.
      
      1. Refunds were counted as revenue. Eighteen queries summed payments on
         status='confirmed' with no direction filter, so outbound refunds were
         added to income across the dashboard, the revenue/product/subscription
         widgets, the financial report, the print report and ReportService.
         That inflated revenue by 40,048 EGP all-time, 32,510 this month.
      
      2. Refunds were simultaneously counted as an expense. The refunded
         original already drops out of revenue when its status becomes
         'refunded', so adding the outbound payment to expenses deducted the
         same money a second time. Refunds are now contra-revenue: the revenue
         card shows gross collected, refunds, and the net, and the expense side
         no longer includes them.
      
      3. Expenses were presented as vague lumps, the worst being "مدفوعات أخرى"
         — which was in fact customer refunds. The breakdown is now one line
         per real cost (payroll, facility rent, purchases, and each expense
         category separately), sorted by size, each stating where it comes
         from.
      
      Payroll was missing from expenses entirely; approved and paid payslips
      plus trainer compensation are now included, scoped by branch through the
      trainer's employee record.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      99c1d2b6
    • Claude's avatar
      Scope financial overview expenses and P&L to the active branch · 944c5001
      Claude authored
      The financial overview filtered revenue by branch but not expenses, so
      every branch showed the same expense figure. The "مدفوعات أخرى" line was
      academy-wide outbound payments — 32,510 EGP of customer refunds issued
      this month, all belonging to Zayed — displayed identically under all 7
      branches. Purchase orders had the same problem, and in the 6-month P&L
      chart both income and outbound were unfiltered.
      
      Also: PaymentService::refund() created the outbound payment without
      copying branch_id from the payment being refunded, so refunds taken
      through that path landed in no branch at all and were invisible in every
      branch view. RefundService already did this correctly.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      944c5001
    • Claude's avatar
      docs: move loose specs out of .claude into docs/specs · aeab7b7d
      Claude authored
      102 KB of planning documents (website-builder-v2 spec, mobile app plan, mobile
      API implementation, data snapshots) were sitting directly in .claude/ rather
      than in docs/. They are reference material, not agent configuration.
      
      .claude/ is now empty of markdown, so nothing in this repo is auto-loaded into
      agent context except CLAUDE.md.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      aeab7b7d
    • Claude's avatar
      docs: move agent rules out of auto-injection, compact CLAUDE.md · f114bf42
      Claude authored
      The 18 files in .claude/rules were injected into every agent turn — 38 KB of
      POS, inventory and attendance rules loaded even while editing CSS. They are now
      in docs/agent-rules/ and read on demand.
      
      CLAUDE.md keeps every hard invariant inline (money as piasters, tenancy scoping,
      double-entry immutability, migration-first, RTL logical properties, no dead
      links, safe_url/clean_html) and indexes the detail, so nothing that protects
      code quality was dropped.
      
      Also documents the deployment constraint that governs every migration: all
      tenants build from main and the entrypoint runs migrate --force plus db:seed on
      every container start.
      
      Net: ~57 KB less agent context per turn.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      f114bf42
    • Claude's avatar
      chore(reference): add OC-Sport site mirror for the website migration · fa497ab9
      Claude authored
      Captured 2026-08-30 from oc-sport.com as the source spec for rebuilding the
      client's site in the v3 builder. Contains their 7 pages in both locales, the
      static assets, the public images, their published OpenAPI description and the
      extracted bilingual content catalogue (604 strings x en/ar).
      
      analysis/FINDINGS.md holds the gap analysis that motivated the v3 builder.
      
      Excluded from the Docker build context via .dockerignore, so it never ships to
      a tenant image.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      fa497ab9
    • Claude's avatar
      fix(website): guard editor-supplied URLs against unsafe schemes · c3f2ae53
      Claude authored
      Menu items, button links, announcement bars, popups, floating buttons and
      navbar CTAs all wrote editor-supplied values straight into href attributes.
      Any user with settings.manage could store a `javascript:` URL and have it run
      for every visitor of that tenant's public site.
      
      Adds safe_url(), which allow-lists http/https/mailto/tel/whatsapp, site-relative
      paths and fragments, and rejects protocol-relative URLs, data:, vbscript: and
      entity/whitespace/control-character obfuscation before testing the scheme.
      
      Applied at four layers so no writer can bypass it:
        - input     MenuManager rules + BlockField Link validation
        - model     WebsiteMenuItem::href()
        - render    every editor-supplied href in every website view
        - import    WebsiteBlueprintService, since blueprint files skip form rules
      
      safe_url() returns null rather than '#', so blocks skip the link entirely
      instead of emitting a dead anchor — this satisfies the project's no-href="#"
      rule with the same mechanism.
      
      Also fixes Alpine expression injection in the gallery and schedule lightboxes,
      where a quote inside an image URL could break out of the inline handler: Blade
      escapes ' to &#039; but the browser decodes it before Alpine parses.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      c3f2ae53
    • Claude's avatar
      feat(website): add page + block tree builder (v3) · 39f468a9
      Claude authored
      The v2 builder could not express more than one page: website_sections had a
      unique(academy_id, section_key) constraint, there was no pages table, and
      SectionManager exposed only toggle + reorder. A client with a seven-page site
      had no way to represent page two.
      
      Adds an additive page/block model alongside v2:
      
      - website_pages + website_blocks (nested tree, JSONB data/style)
      - BlockRegistry of BlockType classes: 31 types, 133 layout variants, 237
        fields, 442 validation rules derived from the field schema
      - Page/Block/Menu/Blueprint services, BlockRenderer, BlockDataResolver
      - Builder UI: page manager, block tree editor, schema-driven field forms,
        repeaters, content/design/motion panels, image upload
      - Authored navigation (website_menus) with dropdowns, replacing nav links
        that were previously derived from enabled sections
      - Blueprint import/export via `php artisan website:blueprint`
      - Extended motion library: entrance effects, delay, stagger, parallax
      
      A new block type now costs one PHP class — no migration, no enum case, no
      CHECK constraint.
      
      Nothing here is destructive. website_sections is untouched and "/" falls back
      to the legacy renderer when no builder homepage exists, so already-deployed
      tenants are unaffected until they opt in.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      39f468a9
    • Claude's avatar
      Fix branch leaks in the two receptionist wizards · c54f5aea
      Claude authored
      The enrolment wizard looked up a programme's base price without any
      branch filter, so with per-branch pricing across 7 branches the
      receptionist could be quoted another branch's price.
      
      The registration wizard's printed receipt resolved the branch with
      Branch::first(), so every receipt printed the first branch's details
      regardless of where the registration actually happened.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      c54f5aea
    • Claude's avatar
      Filter every management screen by the active branch · f282f7bc
      Claude authored
      The system predates branches, and adoption of the branch switcher was
      partial: 44 of 186 Livewire components used UsesBranchScope, and several
      that imported it never actually called it. The dashboard was the worst
      case — half its widgets were branch-aware and half silently reported
      academy-wide totals next to them, so the numbers on one screen were not
      comparable with each other.
      
      OC-Sport runs 7 active branches, so every unscoped widget was showing
      six other branches' data.
      
      Dashboard: scoped trainers-present, pending payslips, pending documents,
      low stock and expiring medical certificates, which were academy-wide.
      All six dashboard widgets (revenue, product revenue, subscription
      revenue, enrolment trends, overdue renewals, trainer dues) now filter by
      branch, including the raw-SQL CTEs in the revenue breakdowns.
      
      Lists and reports: events, evaluations, base prices, pricing rules,
      promotions, stock counts, kits, document approvals, trainers, trainer
      advances, payroll, essential deliveries and the financial report.
      
      Pickers: participant, group, program, facility, warehouse, product and
      employee selectors now offer only the active branch's records, so a
      transfer or invoice cannot silently reference another branch.
      
      POS and InvoiceShow used auth()->user()->branch_id directly, ignoring
      the switcher entirely — a user who switched branch still transacted
      against their home branch. Both now read the active branch.
      
      Deliberately left unscoped: parent- and guardian-facing screens, which
      are scoped to their own children and have no branch switcher, and
      single-record detail screens, which are already scoped by the record and
      would hide legitimately related history for participants who moved
      between branches.
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      f282f7bc
    • Claude's avatar
      Make login credentials case- and format-insensitive · 60eabb70
      Claude authored
      Postgres '=' is case-sensitive, so a user stored as 'Km...@gmail.com'
      could not log in from a phone keyboard that lowercases the email field.
      The lookup in AuthService returned null before Hash::check ever ran, so
      this presented as "wrong password" and was invisible in login_history —
      that table is only written once a user has been found.
      
      On OC-Sport this affected 8 of 26 accounts, and had already produced one
      duplicate registration: a user who could not get in simply signed up
      again with the same address in lowercase.
      
      - CredentialNormalizer: one canonical shape for emails and phones
      - AuthService: case-insensitive email lookup, deterministically ordered
        so a pre-existing case-duplicate pair resolves to the account actually
        in use rather than an arbitrary row; phone lookup matches local and
        +20 forms
      - User: set-mutators so new rows are stored canonical
      - Migration: normalises existing rows, skipping and logging any that
        would collide, since those are duplicate accounts needing a human
        merge rather than a guess
      Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
      60eabb70
    • Mahmoud Aglan's avatar
      Add branded printable schedule sheets with the space grid · 3d472374
      Mahmoud Aglan authored
      "The schedule" is not one document. Different people print it for different
      reasons and a single layout serves none of them, so this ships three sheets
      sized for their actual use scene:
      
      - Facility day board (A4/A3 landscape, auto-picked by segment count) — time
        down the side, the facility's physical grid segments across the top, every
        booking in its own cell. This is the one that carries the grid, and the one
        that did not exist. Built to be pinned at the court entrance and read from
        a few metres away.
      - Facility week board (A3 landscape) — seven days x time, segments as a badge
        per booking. The notice-board overview.
      - Trainer day cards (A4 portrait, two-up, cut lines) — one pocket card per
        trainer: when, where, which segment, how many players. A trainer does not
        want an A3 off the wall.
      
      Rendered as branded HTML and printed from the browser rather than through a
      PDF library: Arabic shaping survives intact, Cairo and the academy's brand
      colour render exactly, and nothing queues on the server. print-color-adjust
      is set explicitly, without which browsers strip every fill and the whole
      colour-coded board arrives as blank boxes.
      
      Group colours move to a shared GroupColor palette used by both the builder
      and every sheet, so a coach who learns "our group is the teal one" on the
      wall sees the same teal in the app. The hues stay distinguishable in
      greyscale, because plenty of academies print on a mono laser.
      
      Bookings read as a filled, tinted cell rather than a thin coloured stripe —
      a 3px rail is invisible at the distance these are actually read from.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      3d472374
    • Mahmoud Aglan's avatar
      Replace emoji with icons; scope schedule builder by branch and sport · 9454aa03
      Mahmoud Aglan authored
      Icons
      - No emoji anywhere in the UI. Extracted the sidebar's inline SVG map into
        a single <x-ui.icon name="..."> component and added the icons the pricing
        work needed, so there is one source instead of a per-view copy. Discount
        recipes now carry icon NAMES, not glyphs.
      
      Schedule builder
      - Facilities are scoped to the selected branch. The screen listed every
        branch's facilities, which is how someone books the wrong building. A
        ?facility_id= carried over from another branch (bookmark, back button) is
        now dropped instead of silently overriding the branch scope.
      - Groups are scoped to the facility's branch AND to the sports that facility
        hosts, so a football court no longer offers swimming groups. That link did
        not exist, so this adds a facility_activities pivot. A facility that
        declares no activities still hosts anything, so nothing breaks for academies
        that have not filled it in.
      
      Facility grid
      - Removed the arbitrary ceilings (rows/columns capped at 10, lanes at 20).
        Physical space is not limited to a number we picked.
      - New facilities never got a layout, which is why the grid silently failed to
        appear on them. FacilityService::create now seeds one, the migration
        backfills every existing facility that has none, and the default is a 1x1
        grid — "one whole space, not subdivided yet" — rather than inventing a
        subdivision nobody asked for.
      - Grid size is editable straight from facility settings, with a live preview
        of the cells being described. Shrinking onto a segment that holds a
        confirmed future reservation is refused rather than silently dropping
        someone's booking.
      - Sports and starting grid are both settable at creation time too.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      9454aa03
    • Mahmoud Aglan's avatar
      Fix pricing engine and rebuild discounts around recipes and a picker · b3f127c8
      Mahmoud Aglan authored
      Two bugs meant no pricing rule has ever applied correctly:
      
      1. Condition keys never matched. The engine reads min/max/values;
         the wizard wrote min_age/min_children/target_gender and the form
         blade wrote a third set. Ranges saw null bounds and list rules saw
         an empty allow-list, both of which passed, so every rule applied to
         every participant.
      2. Percentages were 100x too small. applyAdjustment divides by 10000
         (basis points) but both screens stored a plain percent, so "20%"
         discounted 0.2%.
      
      They masked each other, which is why the symptom looked like a broken
      engine rather than two bugs — and why everyone moved to the untyped
      super-admin price override instead.
      
      Engine
      - ConditionSchema is now the single owner of the conditions vocabulary;
        builder, engine, simulator and migration all read keys from it.
      - Percent handles all basis-point conversion; nothing else touches the
        raw column.
      - evaluateInList fails closed instead of treating an empty allow-list
        as "match everyone".
      - custom rules no longer auto-apply; they are picker-only.
      - enrollment_timing honours days_before_start (fails closed without a
        program start date instead of silently passing).
      - Global discount cap reads system_settings rather than a hardcoded
        constant with a TODO.
      - New: explain(), audience(), wouldApply(), and role-capped manual
        discounts.
      
      Per-branch
      - pricing_rule_branches pivot so one rule targets many branches,
        instead of one near-identical row per branch that drifts apart.
      
      Stacking
      - is_stackable now defaults to false; best-of-one is the normal case
        and stacking is an explicit opt-in.
      
      Authoring
      - The five-step column editor becomes a recipe gallery plus an Arabic
        sentence, with a live simulator on a real participant and an audience
        count that warns when a rule would hit everyone. Saving a
        conditionless rule is refused.
      
      Checkout
      - ManagesDiscounts trait plus <x-pricing.discount-picker>: branch-scoped,
        searchable, pinned favourites, replace-vs-stack inline, blocked rows
        show why. Wired into CollectPaymentWizard renewals; discount names are
        frozen onto invoice.metadata so receipts survive later rule changes.
      - NewRegistrationWizard now prices through the engine using a
        provisional context built from the form, since the participant row
        does not exist yet. The step-4 guard still checks the base price, so a
        100% discount is not mistaken for an unpriced program.
      
      Migration
      - Rewrites conditions onto the canonical keys and scales percentages to
        basis points. Rules whose conditions cannot be mapped confidently are
        deactivated rather than guessed, with the old JSON kept in
        metadata.legacy_conditions.
      
      Also fixes list and coupon views that rendered the raw column (a 20%
      rule would have displayed as 2000%), and adds the [x-cloak] CSS rule
      that was missing app-wide.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      b3f127c8
  2. 27 Aug, 2026 2 commits
  3. 24 Aug, 2026 1 commit
  4. 23 Aug, 2026 7 commits
  5. 22 Aug, 2026 3 commits
  6. 20 Aug, 2026 1 commit
    • Mahmoud Aglan's avatar
      Add group payment/product stats + fix WithSorting trait conflict · 521f6d7b
      Mahmoud Aglan authored
      - Group view: added KPI cards for paid/unpaid/free player counts
      - Group view: added essential product purchase stats with progress bars
      - Group view: added installment info column per player in enrollments tab
      - Fixed PHP 8.2+ trait property conflict: removed $sortBy/$sortDir
        redeclarations from 13 Livewire components that use WithSorting trait,
        moved custom defaults to mount() instead
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      521f6d7b
  7. 19 Aug, 2026 3 commits
  8. 16 Aug, 2026 2 commits
  9. 15 Aug, 2026 1 commit
  10. 13 Aug, 2026 6 commits
    • Mahmoud Aglan's avatar
      Add file attachment to expenses (proof/receipt upload) · e891ce11
      Mahmoud Aglan authored
      - New migration: attachment_path + attachment_name columns on expenses
      - ExpenseForm uses WithFileUploads trait, accepts jpg/png/webp/pdf up to 5MB
      - Drag-drop upload zone in the form with preview of attached file name
      - Files stored in storage/app/public/expenses/
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      e891ce11
    • Mahmoud Aglan's avatar
      Auto-regenerate attendance records when participant reactivated · af0946b7
      Mahmoud Aglan authored
      When a participant goes from suspended/frozen/inactive → active,
      the listener now regenerates expected attendance records for all
      their active enrollments' future sessions.
      
      Previously, reactivated participants were invisible in attendance
      because records were only generated at session creation time.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      af0946b7
    • Mahmoud Aglan's avatar
      Fix POS payment collection + retroactive wizard reliability · 86da35b4
      Mahmoud Aglan authored
      Payment Collection (CollectPaymentWizard):
      - Remove branch filter from participant search (POS sells cross-branch)
      - Include Draft status in outstanding invoice query
      - Add invoice search mode (search by invoice number/contact name)
      - Allow walk-in invoices to be found and paid directly
      - Accept inactive/frozen/registered participants (not just active)
      
      Retroactive Enrollment Wizard:
      - Fix "paid outside system" to use PaymentService (creates proper Payment
        + Transaction records for financial reports)
      - Add server-side validation in confirm() before DB transaction
      - Add national_id duplicate check to prevent duplicate participants
      - Add price=0 guard (show error if no base price and no override)
      - Fix rounding loss: remainder goes to last month's invoice
      - Pass skip_auto_invoice to prevent double invoice creation
      
      POS Partial Payment:
      - Enable allows_partial_payment on all existing products (migration)
        so the deposit/partial payment UI appears at checkout
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      86da35b4
    • Mahmoud Aglan's avatar
      Add financial quick-links: invoices, POS history, expenses list · f1f30cc6
      Mahmoud Aglan authored
      All financial-related pages are now reachable from the Financial Overview
      via quick-action buttons with proper permission gates.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      f1f30cc6
    • Mahmoud Aglan's avatar
      Add sales receipts tab to product detail page · eb96c326
      Mahmoud Aglan authored
      Shows all POS receipts (RCP-*) containing this product with: receipt
      number, customer, total, payment status, method, cashier, date.
      Each receipt has a print link and invoice link for further actions.
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      eb96c326
    • Mahmoud Aglan's avatar
      Redesign branch switcher: bold dropdown with pin/lock feature · e3750618
      Mahmoud Aglan authored
      - Prominent button with colored border (amber pulse when unpinned, green when pinned)
      - Dropdown with branch list, selected indicator, and pin status
      - Pin icon shows when branch is locked
      - Selecting a branch auto-pins it (persists across logout/login)
      - Unpin button available to release the lock
      - Visual hint text explains pin behavior
      Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
      e3750618
  11. 10 Aug, 2026 1 commit