- 05 Sep, 2026 10 commits
-
-
Mahmoud Aglan authored
Terminology, screen-by-screen walkthrough, a demo running order, and — most importantly — an honest status matrix: 267 money paths mapped across 67 modules, 71 of which actually reach the ledger. Section 6 lists what must NOT be demoed or claimed. Being caught overstating in front of accountants is far worse than a known, quantified gap, so the guide leads with the exclusions and gives the exact wording for the hard question. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
cron/runner.php writes a cron_job_log row before every eligible job. That table does not exist, so the runner threw on the first job with shouldRun() === true and none of the 43 scheduled jobs has ever executed: subscription generation, instalment default handling, activity-subscription revocation, academy settlements, coach payroll, monthly depreciation, and every expiry reminder. The container's crontab is present and cron is running — the hourly entry has been firing into an immediate exception the whole time, which is why storage/logs/cron.log does not exist. Creating the table alone would be reckless the night before a finance review: the crontab fires hourly, so all 43 would start on the next tick, and several write off receivables, impose fines, drop memberships and auto-complete waivers (which now post accrual entries). So the runner is additionally gated behind system_config.cron_enabled, seeded to 0. Turn it on from Settings when someone can watch the first run. Until then the runner exits with a clear message rather than pretending to work. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
onPayrollPaid read total_gross and total_net off hr_payroll_runs and grouped hr_payroll_components_log by component_type. None of those columns exist: hr_payroll_runs has gross_earnings / net_salary hr_payroll_components_log has `type`, not component_type hr_payroll_periods has period_code, not period_name Confirmed with SHOW COLUMNS on the live database. The handler threw "Unknown column" on its first query, and the listener only logs, so payroll silently posted NOTHING — no salary expense, no employer insurance share, no withheld tax anywhere in the ledger. It also had the grain wrong. PayrollController dispatches hr.payroll.paid once PER EMPLOYEE; an hr_payroll_runs row is a single payslip, not a whole run, and the period lives in hr_payroll_periods. Every amount needed is on the payslip. Rewritten against the real schema: Dr Salary Expense gross_earnings Dr Employer Insurance Expense insurance_employer Cr Bank net_salary Cr Insurance Payable insurance_employee + insurance_employer Cr Tax Payable tax_amount Cr Employee Loans loan_deduction Cr Other Deductions Payable penalty + absence + other Balances by construction: the payslip satisfies gross - total_deductions = net and the deduction buckets sum to total_deductions. Verified on all three live payslips — e.g. run 1: Dr 15,000.00 + 2,362.50 = Cr 4,402.20 + 3,748.50 + 9,211.80 = 17,362.50. A salary-deducted loan instalment credits the employee-advances receivable rather than being treated as income. Penalties and absence deductions are parked in accrued expenses and registered as a configurable pointer, because whether they belong there or as a reduction of salary expense is a decision for the accountants, not a constant in code. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
onSaleCompleted summed sale_items.total_cost. That column does not exist; the table stores a per-unit cost_price alongside quantity. Confirmed with SHOW COLUMNS on the live database. Every sale therefore threw "Unknown column" inside the sale.completed listener, which is wrapped in a try/catch that only writes to the log. So inventory was relieved in the stock ledger while the general ledger kept carrying it, and no cost of sales was ever recognised — the gross margin on every sale was overstated by its entire cost. Now SUM(cost_price * quantity) over non-refunded lines. This also un-breaks onSaleVoided, which reverses the 'sale_cogs' entry and could never find one. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Assets came to 124,051,895.29 against liabilities + equity of 84,950,136.58. Out by 39,101,758.71 — a balance sheet that does not balance. Cause: the sheet added only the CURRENT fiscal year's net income to equity. No year-end closing entry has ever been posted here (period_closings is empty), so the revenue and expense accounts still carry all-time balances and retained earnings has never absorbed prior years. The earlier years' profit therefore sat in the income accounts and appeared nowhere on the sheet. Verified against the live ledger: liabilities 79,821,436.63 revenue - expenses (all time) 44,230,458.66 (80,820,684.83 - 36,590,226.17) --------------- 124,051,895.29 = total assets, exactly Accumulated profit now runs from the first posted entry rather than the fiscal year start. This stays correct after closing entries begin: a closing entry moves the profit into retained earnings and zeroes the income accounts, so the figure then covers only post-closing activity while the closed profit sits in the equity accounts. The current fiscal year's slice is still returned separately, as current_period_net_income, because that is what the board asks about. Note for the chart: there are no accounts typed 'equity' at all — capital (2101) and retained earnings (210201) are typed 'liability'. That is why total_equity consists solely of the accumulated-profit line. The sheet balances either way, but the classification is worth revisiting with the accountants. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
`reservations` identifies its booker with booker_type plus player_id / member_id. There is no booker_id column — confirmed with SHOW COLUMNS on the live database, not from the migrations. Five call sites queried it anyway, so every one of them threw a SQL error: FacilityDashboards/Controllers/FacilityDashboardController.php (x2) PlaygroundAdmin/Services/ClubDashboardService.php (x2) PlaygroundAdmin/Services/PlaygroundMirrorService.php (x3) FacilityGrids/Services/PoolFinancialService.php (x1) PlayerApi/Services/PlayerBookingService.php (x4) Effect: the facility dashboard, the club-wide playground dashboard, the pool financial panel and the playground mirror hard-500 on every load, and the player app could never create a booking — the INSERT named booker_id too. That matches the data: 7 reservations exist with booker_type set and player_id/member_id both NULL, and zero player bookings. Reads become COALESCE(player_id, member_id); the joins key on the specific column for their booker_type; the INSERT writes player_id. Also PlaygroundMirrorService queried private_match_bookings.match_date, which is booking_date on that table. (live_matches genuinely has match_date, so MatchCenter is untouched.) And sa_bookings / pool_bookings really do have booker_id, so those references are correct and left alone. Every rewritten query was executed against the live database before committing. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The opening figures live in TWO places in this ledger: the chart_of_accounts.opening_balance column AND 24 posted journal entries dated 2024-07-01 with reference_type='opening', totalling 90,601,962.36. Three reports read the column and then also summed the ledger movement that already contained those same entries, counting the opening twice: - Trial balance (LedgerService::getTrialBalance) - General ledger (LedgerService::getAccountLedger) - Balance sheet (FinancialReportService::getBalanceSheet, and the consolidated sheet which delegates to it) Measured on live data, trial balance over FY 2024/2025: 1103 مشروعات تحت التنفيذ reported 85,627,410.75 actual 43,923,543.75 210201 أرباح مرحلة reported -146,645,270 actual -73,322,635 i.e. exactly double on every account carrying an opening balance. The report still footed, because opening balances net to zero across debit and credit — so it looked right and every line was wrong. Only periods containing 2024-07-01 were affected; a 2026 trial balance was already correct. The opening column is now derived as cumulative posted movement BEFORE the period start, which is the standard definition, removes the double count structurally, and works for any period rather than only a year boundary. The trial balance query is also restructured into two independent aggregates so no row multiplication is possible and an account whose only movement predates the period still appears. Income statement was already correct and is unchanged. Also in this commit: - LedgerService::rebuildBalances() + a seed that runs it. The opening import wrote journal rows without going through JournalService, so 24 accounts had a cached current_balance disagreeing with the ledger — retained earnings cached 0.00 against an actual 73,322,635.00. The reports read the ledger and were fine, but the Chart of Accounts screen and the bank-reconciliation opening figure read the cache, which is precisely where an accountant would find a number contradicting the trial balance. - Carnet guest entry never posted. Accounting listened on 'carnet.guest_entry_recorded'; GuestEntryService dispatches 'carnet_guest.entry_recorded' (underscore, not dot). Notifications listens on the correct name, which is why notifications worked and the ledger entry never appeared. Fees were recorded in carnet_guest_entries.amount_paid and posted nowhere. - 'tournament.fee_collected' has no dispatcher anywhere. Documented as dead rather than left looking wired. - Two fiscal years were flagged is_current; the seed leaves exactly the one containing today. FiscalYear::findByDate now resolves overlapping years deterministically (open first, then narrowest range) instead of taking whatever the database returned — this chart has calendar years overlapping a July-June year, so Jul-Dec 2024 matches two. No entry is reassigned; all 795 are already inside their assigned year. - PostingRouter and postViaRule now probe App::db() with try/catch. It is typed `: Database` and throws when unbound rather than returning null, so the previous null guards could never fire. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Generalises the revenue engine from "collection" to every stage a document posts at, and routes all 26 auto-posting paths through it. Two new dimensions on a rule: stage accrual | collection | payment | refund | writeoff | transfer direction inflow → counter account DEBITED, allocation lines CREDITED outflow → allocation lines DEBITED, counter account CREDITED So the same allocation maths now drives revenue, expense, receivable and payable postings. Contra-revenue is always a debit regardless of direction. Where the amounts are computed elsewhere and only the accounts need to be configurable — payroll components, treasury legs, COGS, rental legs — a second mechanism (PostingRouter::accountFor) resolves a configurable account pointer instead of forcing those through the allocator. Both are edited from the same screen. Dead posting paths fixed. Each of these targeted a header account, which JournalService refuses, and the callers only Logger::error — so they have been failing invisibly: - 230601 الموردون is a header → the ENTIRE procurement cycle (vendor invoice, vendor payment, return-to-vendor) could never post. Now 230601002. - 310103 حصة الشركة في التأمينات did not exist at all → payroll dropped the employer insurance line, then a balancing fallback silently increased the bank credit to force the entry to balance, misstating cash. The account is created, and an imbalance now refuses to post and reports instead. - 230804 جاري مصلحة الضرائب is a header → rental VAT could never post. Now 23080404 ضريبة القيمة المضافة. - AccountCodes::INPUT_TAX resolved to 120408 مدينو بيع أوراق مالية, an unrelated account. Input VAT now posts to 12041106. - Member write-off debited MISCELLANEOUS_REVENUE. A bad debt is an expense; it now posts to 3328 ديون معدومة. - $result['entry_id'] is never returned by JournalService (the key is journal_entry_id), so rental invoices, treasury settlements and treasury deposits never linked back to their journal entry. - SUB_TREASURY_CASH points at 12060102 الصندوق بالدولار, the USD box. Left deliberately unmapped and surfaced on the diagnostics page so finance picks the right EGP account rather than having one guessed for them. Accruals now also create the accounts_receivable sub-ledger row alongside the GL entry, which is why that table was empty against 970,592.67 EGP of scheduled instalments. Verified against a full clone of the production schema and chart of accounts in a throwaway database: all six stages post balanced entries, VAT 14% inclusive on 1140 yields 1000 revenue + 140 tax, a five-line split (two fixed + two percentage + remainder) balances to the piastre, and a 12,000 annual subscription produces exactly 12 monthly deferral rows summing to 12,000 with the recognition run posting the current period. 27 allocation unit tests pass. Seeded rules reproduce existing behaviour except where that behaviour was a silent failure. Unconfigured stages still fall through to the legacy path. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Whoever can read the chart of accounts can read where revenue lands; whoever can change it can change the mapping. Without this the محاسب role sees the Accounting menu but gets 403 on the revenue-mapping screen. super_admin holds the '*' wildcard and needs no explicit grant. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Replaces the hardcoded AccountCodes::creditAccountForPaymentType() match statement with a versioned, effective-dated mapping that finance controls from /accounting/revenue-mapping. Every collected amount can now be split across multiple GL accounts by flat amount, percentage, or remainder, with VAT handled as its own layer and deferred revenue amortised over the service period. What the live DB showed, and this addresses: - 4,256,399.96 EGP across 129 transactions posted to a single catch-all account (410515 إيرادات متنوعه) — waiver, separation, death, foreign membership, early settlement and four payment types that had no rule in the code at all and silently fell through to `default`. - 240,582 EGP of divorce fees posted to 410302 «محل 1», a shop rental account. - 120301 العملاء and 230804 جاري مصلحة الضرائب are header accounts, and JournalService rejects posting to headers — so every AR and VAT entry has been failing silently. accounts_receivable holds 0 rows against 970,592.67 EGP of unpaid instalments. Model follows SAP account determination / Dynamics posting profiles, adapted to Egyptian VAT law 67/2016 and EAS 48 revenue recognition: - revenue_streams catalogue of every chargeable thing - revenue_tax_profiles rate + inclusive/exclusive + treatment - revenue_posting_rules versioned, effective-dated, scopeable - revenue_posting_rule_lines the split components - revenue_posting_log which rule version produced which entry - revenue_recognition_schedules deferred revenue amortisation Allocation order is fixed and deterministic: tax extraction, then fixed amounts, then percentages, then a mandatory remainder line that absorbs rounding residue so the entry always balances. Tax is a separate layer rather than a split because inclusive and exclusive pricing are not the same number: 14% of a tax-inclusive 1140 is 140 on revenue of 1000, not 159.60. Deferral is separate for the same reason — it is a split across periods, not accounts. Adds two postable accounts the chart was missing: 120301004 أعضاء النادي (مدينون) and 12041106 ضريبة القيمة المضافة — مدخلات. Seeded rules reproduce current posting behaviour exactly, so this deploy moves no reported number. Streams landing in a catch-all are flagged for review rather than silently re-pointed — repointing them moves real revenue between accounts and is finance's decision. Unconfigured streams fall through to the legacy path unchanged. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 04 Sep, 2026 1 commit
-
-
Mahmoud Aglan authored
The microsite is built for scrolling, so a naive print lost most of it: reveals start invisible, the phone shows one prototype screen at a time, the portal shows one admin screen, and <details> print collapsed. - @media print in styles.css: force reveals visible, drop the fixed nav and prototype tools, start each section on a fresh page, and mark cards, timeline items, tables and price blocks break-inside:avoid so none is split across a page boundary - generate-pdf.mjs (puppeteer): expands both prototypes before printing — the single phone frame becomes a labelled 3x3 grid of nine real app screens, and all seven portal screens are stacked. 1240x1754 pages (A4 proportion at 150dpi), backgrounds on. - Adds a "تحميل العرض PDF" button to the hero; the print stylesheet hides .btn-row so it does not appear inside the PDF itself Output is 14 pages with a real text layer: Arabic extracts correctly and figures stay searchable. Ghostscript compression reaches 2.3MB but its font re-embedding drops Arabic strings from the text layer, so the uncompressed 8.5MB file is kept instead. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 03 Sep, 2026 5 commits
-
-
Mahmoud Aglan authored
Commercial terms: - Price 900,000 -> 500,000 EGP, breakdown rebased to sum exactly (230k app + 160k portal/CMS + 60k gate/invites + 50k launch) - Payments simplified from three milestones to two: 360,000 on signing, 140,000 on delivery - Timeline 4-6 weeks -> 2-4 weeks, timeline recompressed from five milestones to four - Early-signing discount recalculated: 5% = 25,000 (was 45,000) - "غير شاملة ضريبة القيمة المضافة" now stated in the price hero, the totals row, under the payment schedule, in the hero stat and footer, and bolded in the FAQ New scope — club news / blog: - Fourth axis added; news moved out of the deferred list into phase one - Live prototype gains a news feed, article page and a fifth tab, with home showing the two latest items - Portal gains a news management screen: article list with reach stats, editor with category, image drop and publish-notification toggle - Article artwork is a branded crest placeholder, not stock photography — the club supplies real images at launch and inventing them would misrepresent what has been approved Reduced technical detail per request: - Dropped the per-template column-name lists (six of them) and the three import-engine cards, replaced with one plain note - Removed API/OTP/RTL/iOS-14/Android-8 jargon throughout; rewrote the security and extensibility cards in business terms - Simplified the in-scope list wording Also makes .tbl scroll rather than clip: a table too wide for its column silently lost its last cells instead of becoming reachable. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The prototype in the proposal was eight inline <div>s toggled with display:none — screenshots in a phone bezel. It now embeds a real single-page app from app/, so the screens shown in the proposal are the screens that ship, and the board can actually use it. app/ — hash routed, so any screen is linkable (#/dues): splash, login, otp, home, dues, pay, paying, success, receipts, qr, invites, activities, activity/:id, schedule, notifications, profile State is live, not scripted. Selecting dues recomputes the total before paying; paying clears those dues, creates a receipt and posts a notification; issuing an invite decrements the balance; subscribing to an activity adds the subscription, consumes a place and generates the first invoice into المستحقات. The QR regenerates every 60s against a countdown ring. Dark/light theme persists. Verified end to end by driving the app in headless Chrome, not just by rendering it. Chose an SPA over the multi-page pattern used by the older Proposal/ prototype: no white flash between screens, real forward/back transitions, and shared state across screens, which is the whole point of showing a collection flow. Proposal integration: - Phone hosts <iframe src="app/">; the side list drives it over postMessage and the prototype reports its route back, so the list and the annotation stay in sync when someone navigates inside the phone - Theme toggle, reset, and open-fullscreen controls - Annotation panel rewritten per screen Removed 409 lines of static screen markup and the 93 lines of CSS that served it (.sbar/.app-hd/.mcard/.tabbar/.sport-item/.otp-row/.qr-*), verified dead by checking real class= usage, not substring matches. Adds .dockerignore: the image is built with `COPY . /usr/share/nginx/html`, so any stray working file in this folder would be served publicly. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Emoji render as platform-specific colour cartoons (Apple/Windows/Android each differ) which reads informal in a document going to a club board, and they cannot inherit brand colour. Replaced every one with a 33-symbol inline SVG sprite: 24x24, stroke-based, currentColor, sized in em so each existing icon slot keeps its own scale. Removed: swimmer, bell, receipt, credit card, mobile, bank, football, martial-arts, tennis, cartwheel, page, lock, floppy, plug, envelope, gear, up/down arrows, and the EG regional-indicator flag pair. Also converted the geometric glyphs sitting in icon slots (fisheye, diamonds, house, quadrant-circle, square-fill, clock) plus the list check/x/arrow marks, so the icon layer is uniformly SVG rather than a mix of text glyphs and emoji. A source scan for emoji ranges now returns clean. Sprite is hidden with position/width/height rather than display:none, which can break <use> resolution in some engines. Fixes an unrelated pre-existing contrast bug found while verifying: .card h4 is declared after .dark h4 at equal specificity, so the three cards in the dark flow section rendered navy-on-navy. Added .dark .card h4. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Copy — the previous register read as marketing/AI boilerplate to a board audience. Removed the tells and rewrote in institutional MSA: - Drop staccato fragment headlines and their trailing periods ("ثلاثة محاور. لا أكثر." -> "نطاق المرحلة الأولى: ثلاثة محاور") - Drop rhetorical punchlines ("الاحتكاك يقتل الاشتراك."، "ليست صورًا تخيلية."، "من ينسى، لا يدفع.") for descriptive prose - Drop the «مصنع البيانات» metaphor and the pitch-deck eyebrow "لماذا الآن" - Rewrite all 8 prototype screen annotations from slogans to labels ("البوابة تعرف من يدخل" -> "الدخول بكود QR") - Reduce rhetorical em-dashes; keep structural ones only Schedule — project is 4-6 weeks, not 12. Timeline recompressed from six milestones over 12 weeks to five over 6, with the 4-week case stated as conditional on data and accounts landing in week 1. Updated hero stat, scope lede, price card, plan heading and footer badge to match. Design — elegant crest watermark: - Oversized club crest bleeding off the inline-start edge of the hero, gold-tinted, offset so it does not double with the hero logo - Alternating-side crest ghosts on problem/scope/flow/price sections - Crest in the price card and footer, plus a gold hairline on the footer - Uses background-image + filter, not mask-image: masks give a cleaner silhouette but do not paint in headless Chrome, so this variant is the one that can actually be verified before shipping - Logical properties throughout (inset-inline, margin-inline) so the watermark mirrors correctly in RTL - Added a prefers-reduced-motion guard for the scroll reveal Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Proposal microsite for Nady El-Seid mobile app (sayd-mobile). Arabic copy: - Fix agreement/tamyiz errors: اكتملت العدد -> اكتمل العدد, متأخر يومان -> يومين, "92 يوم" -> "92 يومًا", منها 214 متأخر -> متأخرًا, يومان تدريب -> يوما تدريب (dual mudaf drops nun) - Fix ambiguous/wrong forms: فيتحدث رصيده -> فيُحدَّث رصيده فورًا, الحمام الأولمبي -> حمام السباحة الأولمبي - Remove translationese: comma-lists rewritten with و, passives given back their agents, Egyptian تشتغلون -> تبدأون التشغيل - Unify register to plural address (اضغط -> اضغطوا, شاهد -> شاهدوا) - Apply reviewed headline/lede/ROI rewrites with corrected orthography Commercial terms: - Price 700,000 -> 900,000 EGP; breakdown rebased to sum exactly (400k app + 290k portal + 110k gate/invites + 100k integration) - Remove all post-launch support: annual maintenance contract, 6 free months, 1-year warranty, 99.5% SLA, first-year store fees. Delivery and release only, stated explicitly in a "غير مشمول" block and FAQ - Payments 40/30/20/10 -> 40/25/35: signing, data-entry plan approved + club opens required accounts and services, completed delivery - Timeline week 1-2 renamed to cover data-entry planning and names the account-opening as a parallel club obligation Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 01 Sep, 2026 3 commits
-
-
Mahmoud Aglan authored
-
Mahmoud Aglan authored
Pre-existing uncommitted working-tree change, not part of the member search work. Committed separately so the 478-line reduction stays visible in history. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Overhaul /members/search so one bar finds anyone in the club — the member, a spouse, a child or a temporary member — and add an advanced filter panel. MemberSearchService becomes the single source of truth for people search: - UNIONs members + spouses + children + temporary_members into one normalised row per matched PERSON (person_type, relation, parent membership, rank). - Token AND matching on names, so word order no longer matters: "محمود احمد" finds "أحمد سيد محمود". - Arabic orthographic folding (أ إ آ ٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي) applied to both the query and the column, so "احمد" matches "أحمد". - Arabic-Indic and Persian digits folded to ASCII before identifier matching. - Relevance ranking: exact membership number / national id, then name prefix, then substring. LIKE wildcards in user input are escaped. Scopes (member/spouse/child/temporary) and fields (name, membership number, national id, phone, form number, passport) are selectable; branch, membership status and membership type filter on the parent membership. A scope whose table lacks the requested field is skipped rather than matching nothing. The legacy search() keeps its exact signature and output shape, so the three existing API consumers are untouched. Also: - Split Member::getStatusOptions() (statuses an employee may ASSIGN) from getAllStatusLabels() (every status, for display/filtering). deceased, transferred and waived exist in live data but were missing from the list, so they could not be filtered on; they are deliberately kept out of the assignable set because the Death, Transfer and Waiver workflows own those transitions. - Dependent deep links honour spouse.view / child.view / temp.view and fall back to the membership file when denied. - Map children.relationship (son/daughter) and temporary_members.category (nanny/parent/unmarried_daughter) to Arabic for display. - The search form submitted to /members, dropping most of what was typed; it now posts back to /members/search. - Sidebar declared member.search while the route requires member.view; aligned. Architecture Map and Dependency Graph updated per project protocol, including the placeholder-ordering constraint in buildScopeQuery() and the three inline member-search SQL blocks that remain unconsolidated. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 31 Aug, 2026 2 commits
-
-
Mahmoud Aglan authored
Phase-1 scope only: membership renewals/installments, sports activity invoices, QR gate entry + invitations. Interactive HTML prototypes for 8 mobile screens and 6 portal screens, 6 downloadable CSV import templates, 12-week plan, 700,000 EGP commercial offer. Deployed to CapRover as app 'sayd-mobile'. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Client clarified their earlier request: the member "special discount" dropdown should show board-approved special_discounts AND active عروض مجلس الإدارة (board_offers) cash discounts side by side, not one instead of the other — they'd stopped seeing anything they'd added under Board Offers. - Add members.special_discount_source ('special_discount'|'board_offer') and drop the hard FK on special_discount_id (a single column can no longer FK exactly one table). Integrity is now validated in MemberController::parseDiscountSelection(). - BoardOffer::allActiveWithCashDiscount() surfaces board offers that define a cash discount as selectable options. - SpecialDiscountService::resolveAssignedDiscount()/amountForType() give one place that normalizes "the member's assigned discount" across both source tables — used by BillingService's invoice line item, the show page's applied-discount banner, and the dropdown's own validation. - fill-form/edit/show views render two <optgroup>s ("عروض مجلس الإدارة" / "الخصومات الخاصة") with prefixed option values (bo:<id> / sd:<id>) so a single form field can select from either table; edit.php's live discount-amount preview now handles fixed-amount discounts too, not just percentage. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-
- 30 Aug, 2026 4 commits
-
-
Mahmoud Aglan authored
fix(members,subscriptions): drop due-date column from family tables, collect annual subscription via الخزنة - Remove the "تاريخ الاستحقاق" column from the spouses/children/temporary members tables on the member show page (display-only, no schema change). - Annual subscription payments no longer post directly from the اشتراك سنوي page. SubscriptionController::payYear() now queues a payment_request instead of calling PaymentService::processPayment() directly; the subscription rows are only marked paid once خزنة العضويات (Membership Treasury / Cashier) actually collects it, via a new payment_request.completed listener (SubscriptionSyncService::completeFamilyYearPayment()). - Closed the same bypass on the legacy generic /payments/process/{id} page, which had its own divergent partial-payment, oldest-year-first logic for annual_subscription that skipped the treasury entirely and violated the all-or-nothing-per-family rule; that path now redirects to the member's subscriptions page instead. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
fix(pricing): require board decision for member special discounts, allow fixed-amount down payment on board offers - special_discounts gains board_decision_number/board_decision_date; the member-facing "special discount" dropdown (fill-form, edit, show, apply) now only lists/accepts discounts backed by a board decision instead of the full unaudited special_discounts catalog, per client request that the dropdown should only read board-approved discounts. - board_offers gains inst_down_payment_type (percentage/fixed_amount) so a board-approved offer's installment down payment (المقدم) can be a fixed cash amount, not only a percentage. Wired through BoardOfferService, InstallmentCalculator (new min_down_amount override) and PaymentLifecycleService so the fixed amount is actually enforced at billing time, not just in the admin form. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Users saw sidebar links that returned 403. Root cause was drift between four independently-authored declaration sets that nothing reconciles: the permission catalogue (bootstrap.php), the route gate (Routes.php), the menu gate (MenuRegistry) and the role grants (seeds). Route shadowing (Router::dispatch is first-match-wins over a sorted module glob): - GET /reports was declared by both Members and Reports; Members won and enforced member.reports while the sidebar gated on report.view_membership. Members' report routes moved to /members/reports/*. - GET /sports-dashboard[/export] was declared by three modules, so the dashboard index and its drill-downs were served by different modules. Disciplines -> /disciplines/dashboard, PlaygroundAdmin -> /playgrounds/dashboard[/export]; /sports-dashboard is now wholly owned by SportsDashboard. - Members/Routes.php used unconstrained {id} in 15 routes, so /members/<anything> was swallowed by MemberController@show. Constrained to {id:\d+}, matching every other module. All 25 affected links updated. Gate alignment: - Six menu entries gated on a different permission than the route they link to (/members/search, /sports, /carnets, /rentals/entities, /notifications/templates, /reports). Authorization bypasses: - RetroactiveWizardController hardcoded a role_code = 'super_admin' query, throwing "هذه الأداة متاحة فقط لمدير النظام". Replaced with a registered member.retroactive permission enforced by the route and grantable via the Roles UI. - report_definitions.required_permission was stored and displayed but never checked, so report.view_membership was enough to open ANY report by code, including financial ones. Now enforced on view/export/print; the listing filters to what the viewer can actually run. Role grants (Phase_105_001, idempotent): - Closes the reported gaps for report_viewer, general_manager, receptionist, sports_officer, academy_manager and membership_director; grants the sports report keys to board_member/auditor so enforcing the per-report permission does not silently remove reports; revokes member.view/member.search from facilities_manager, who keeps bookings and reservations. Data correctness: - SaFinanceReportService read base_price from sa_pricing_rules, a facility booking table with neither that column nor activity_type, and derived revenue as headcount x a rate-card price. Now sums actual sa_registrations .registration_fee, matching how subscription and booking revenue are computed. Regression guard: - php cli.php permissions:audit reconciles all four declaration sets, reproduces the router's load order, and exits non-zero on drift. Run it after touching any Routes.php, menu block or role seed. Docs: new docs/architecture-maps/Authorization.md; cross-module authorization section added to DEPENDENCY-GRAPH.md. Note: the live DB was unreachable from the dev environment, so role grants were verified by replaying the seeds and schema came from migrations, not the live DB. PHPUnit is not installed locally; all changed files lint clean. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- ActivitySubscriptions: fix generate/calculateRate querying nonexistent `enrollments` table; use `academy_enrollments` with correct columns - SportsDashboard: fix queries against nonexistent `disciplines` table; use `sport_disciplines` - Sports: unify conversion-fee percentage to a single source (MembershipRulesService::getAthleticMemberConversionRules), preventing the eligibility preview from drifting from what's actually charged - SportsActivity: align absence-threshold fallback defaults between AttendanceRuleService and TrainingAttendanceService via a shared constant - SportsActivity: auto-bill first month on Registration Wizard completion, matching the direct-enrollment path so wizard-registered players aren't left uncharged until the monthly batch runs - ActivitySubscriptions: guard paySubscription() to only transition pending/overdue -> paid, making it idempotent against the (currently unreachable) payment.completed listener path - ActivitySubscriptions: dispatch academy.enrollment_created from the enroll wizard so PlayerAffairs' auto-billing listener actually fires Also adds/updates Architecture Maps for Sports, SportsActivity, SportsDashboard, ActivitySubscriptions and the cross-module Dependency Graph, per this repo's mandatory architecture-map workflow. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Co-Authored-By:
Claude Sonnet 5 <noreply@anthropic.com>
-
- 29 Aug, 2026 2 commits
-
-
Mahmoud Aglan authored
Every user previously saw the same dashboard: DashboardDataService::getData() returned one fixed payload with no reference to the current employee. A cashier got membership stats they could not act on; an HR manager got revenue instead of headcount. Each role now gets a curated dashboard. Role presets pick the layout, permissions gate every widget (mirroring MenuRegistry::getVisible), and multi-role users get the deduped union of their presets. Super admin gets a 5-KPI, 16-widget command center across six sections. Wires up WidgetRegistry, which existed fully written but was used by nothing. 144 widgets, all SQL executed and verified against the live schema — 46 were corrected during verification, including a month-to-date figure compared against a full prior month (a fake collapse every month), spouse counts missing their status filter, and receivables that included debt owed by archived deceased members. Only the headline plus first six widgets query on load; the rest hydrate through GET /dashboard/widget/{key}, which re-checks permission server-side and renders via the same partial as the eager path. Employees with no mapped role fall back to the previous shared dashboard, preserved verbatim. Also loads Chart.js, which PlayerAffairs has always called behind a `typeof Chart !== 'undefined'` guard while the library was loaded nowhere — those evaluation charts were silently dead and now render. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
-
- 28 Aug, 2026 2 commits
-
-
Mahmoud Aglan authored
Assigns human-friendly usernames, unique passwords, and business-appropriate roles to all 62 imported employees based on their HR job titles. Includes credentials reference sheet for permission testing. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Add board offer "عروض عضويات شيراتون 2026" with 4 tiers (cash 10%, 24mo 0%, 40mo 15%, 60mo 15%) - Add "فوق المتوسط" qualification at 187,500 EGP - Fix crash in show.php: board_offers uses title_ar not name_ar - Rewrite discounts tutorial with full regulatory discount coverage Co-Authored-By:Claude Opus 4.6 (1M context) <noreply@anthropic.com>
-
- 27 Aug, 2026 1 commit
-
-
Mahmoud Aglan authored
Sports Activity Reports: player reports with filters (discipline/program/group/ player type/medical/payment status/branch) and finance reports (revenue/costs/ profit with daily/weekly/monthly/yearly/3yr/5yr/custom periods). CSV and PDF export for both. Role-based access with 3 new permissions. Membership Discounts: fix BillingService to include regulatory discount as bill line item, add regulatory discount section to edit page, add FYI discount guide to show page covering all 3 discount types (special, regulatory, board offers), handle regulatory discount in update controller with document upload. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 26 Aug, 2026 4 commits
-
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Phase 3: Weekly work schedules - Migration: hr_weekly_schedules table (per-employee per-day shift times + rest days) - Seed: import 59 employees' weekly schedules from club's work schedule sheet - Seed: 13 shift definitions (9-5, 3-11, 10-6, 12-8, etc.) Phase 7B: Job description cards - Migration: hr_job_descriptions table (purpose, authority, duties per job title) - View: printable بطاقة الوصف الوظيفي matching club's official template - Route: GET /hr/job-titles/{id}/description-card Phase 8: Org structure hierarchy - Migration sets parent_id=1 (EXEC) for all other departments Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Phase_05_001: changed department_type from 'edara' to 'idara' (CHECK constraint only allows 'idara'/'qism') - Phase_05_002: create employees table records before hr_employee_profiles to satisfy FK constraint - Generates unique usernames (emp0001, emp0002, ...) - Sets default password 'Club@2026' with force_password_change=1 - Links hr_employee_profiles.employee_id to newly created employees.id Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Phase 1 - Leave System (2025 Law): - Annual leave: 15d (1st year) → 21d (after 1y) → 30d (10y+/age 50+) → 45d (disability) - Maternity: 90 → 120 days (4 months), hajj: now PAID with 5y service min - New leave types: childcare (unpaid, 3 career, 24mo gap), paternity (1d, 3 career), exam - Casual leave: enforce max 2 consecutive days - Childcare gap enforcement, service months validation Phase 2 - Employee Import: - Extract 75 employees from HR registry Excel → JSON seed data - 34 Bank of Alexandria accounts extracted for salary transfers - 14 departments + all job titles auto-created from registry - Migration adds variable_salary + total_allowances columns Phase 4 - Payroll Alignment (July 2026 format): - Solidarity fund: 0.25% of gross (صندوق التكافل) - Stamp duty: 3% of net (دمغة عادية وإضافية) - now percentage-based - Emergency fund + VAT config keys added - calculation_json includes full breakdown Phase 5 - Bank Transfer Export: - BankTransferService generates transfer data from payroll runs - CSV export with BOM for Arabic compatibility - View + route: /hr/payroll/periods/{id}/bank-transfer Phase 6 - Performance Evaluation: - Seed 10-criteria template matching club's official form (100 points) - Dual evaluator support (direct manager + general supervisor) - 5-tier rating labels (ضعيف → ممتاز) Phase 7 - Reports & Forms: - Work receipt printable form (إقرار استلام العمل) - Workforce statement report (بيان القوة الفعلية) Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 23 Aug, 2026 3 commits
-
-
Mahmoud Aglan authored
Always-visible reference table on the membership form showing: - All 7 articles with their discount percentages and conditions - Which branches each applies to - What proof is needed (auto-verify vs document upload) - Non-stacking rule note Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Add regulatory_discount_id/amount/document columns to members table - Add eligibility type selector with conditional sections per type - Auto-verification for cross-branch (checks members DB) and club employee (HR) - Document upload for types requiring manual proof (gov, ministry, board, group) - AJAX eligibility check button calls /pricing/regulatory-discounts/check-eligibility - Creates audit trail application record on form submit (status: pending) - Dynamic UI: shows/hides relevant fields based on selected type Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Full-cycle implementation of club bylaw discount rules: - Art 97: Cross-branch member discounts (50% Sheraton
↔ 6th Oct, 25% →Admin Capital) - Art 98: Government employees 50%, Ministry of Youth 62.5% at Admin Capital - Art 99: Ministry of Youth 25% at Sheraton/6th Oct - Art 100: Board of Trustees 50% + 2yr interest-free installment - Art 101: Ministry employees installment-only (no discount) - Art 102: Club employees (5+ yrs) up to 15% - Art 110: Group membership tiered (5-10→3%, 11-20→7%, 21+→10%) Includes: migration, seed data, model, service with eligibility engine, controller (CRUD + eligibility check API + application workflow), views (index, form, applications), routes, permissions, menu entry, and PricingEngine integration. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 20 Aug, 2026 2 commits
-
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Board Offers: - Add board_offer_tiers table for multiple payment plans per offer - Each offer now supports 4 tiers: cash (10% discount), 24mo/0%, 40mo/15%, 60mo/15% - View shows all tiers as selectable cards with pre-calculated breakdowns - Controller accepts offer_tier_id and uses tier-specific terms - Seed populates tiers for all active board offers - Legacy fallback preserved when no tiers are configured Subscription Fix: - First-year members no longer get subscription rows (membership fee covers current FY) - First-year dependents no longer get subscription rows (addition fee covers current FY) - MembershipValidationService bypasses subscription check for first-year members - AutoFreezeService.checkSubscriptionBlock bypasses for first-year members - MembershipRulesService.canPrintCarnet bypasses for first-year members - SubscriptionGenerator skips first-year members/dependents entirely - Individual subscription pay() now redirects to payYear() (all-or-nothing family payment) Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 19 Aug, 2026 1 commit
-
-
Mahmoud Aglan authored
The hidden input had min=1 with value=0, causing browser validation error "not focusable" when form submits. Changed to min=0 with empty default. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-