Changelog — PropertyPro
All notable changes to this app are recorded here. Newest entries on top.
[2026-05-22]
- Brand-new tenant app. Tenants now get a clean, app-like dashboard when they sign in — a Home screen with what they owe and a "pay now" shortcut, plus tabs for Charges, Repairs, Messages, and Account, with Projects and Documents on the home screen. Same polished look as our other apps.
- Tenants can sign themselves up. Share your sign-up link and a tenant can request an account with their phone and a PIN. You approve each request before they can sign in — pending requests appear right at the top of your Tenants list (and on the tenant's profile) with one-tap Approve/Reject. Turn self-sign-up on or off under Settings → Tenant Sign-ups. The staff sign-in link and tenant sign-up link both have a one-tap Copy button in Settings.
- Pay rent online from the portal. Tenants can pay a charge's balance by card (Paystack or Stripe) right from their dashboard, and pay their share of shared projects too. Payments can't be double-counted.
- Report repairs from the phone. Tenants open a maintenance request, attach a photo, and message back and forth with you until it's resolved.
- Dedicated staff sign-in. Staff now have their own sign-in link that drops them straight onto the management dashboard — no more landing on a tenant page. Old sign-in links still work.
- See and share a tenant's PIN. On a tenant's profile you can now view, copy, reset, or clear their portal PIN, so it's easy to help someone who forgot it.
- View as tenant. Open any tenant's portal exactly as they see it (a purple banner reminds you you're viewing as them) — handy for support.
- Export reports to CSV. Download payments, unpaid charges, and who's-owing as spreadsheets from the Reports page.
- Charge wording fixed throughout the tenant view. Charges now read in plain rent language (New → Sent → Due → Overdue → Paid) instead of leftover laundry-style wording.
- Fixed: managers couldn't change a ticket's status or manage projects. The status buttons (Open/In Progress/Resolved/Closed), Close/Reopen, worker assignment, and the project controls (mark funded, close, record contribution, add/remove tenants) were hidden for everyone — they now show again for managers and staff with the right access.
- New properties inherit your price list automatically. When you add a property, its rate card (all categories and items) is copied from your first/default property, so you don't have to re-type every price. You can still edit the new property's prices afterwards.
- Delete a property. Admins can now delete a property from the property switcher (trash icon). It removes the property and everything in it — tenants, charges, payments, projects, tickets, documents, and price list — after a clear confirmation. You can't delete your last remaining property.
- Fixed: adding a tenant could fail with a database error. If the phone number was already used by another tenant (including one under a different property), the form threw a raw error. It now shows a clear "this phone already exists" message, and adding tenants with new numbers works normally.
- Fixed: the Project page wouldn't load (stuck on the loading placeholders). A code error in the contributions list stopped the whole page from running; it loads correctly now.
- Fixed: the Activity Log failed to load with a database error. It now lists every action correctly.
- Fixed: staff signed into the app no longer see your other apps or the platform menus (Marketplace, Billing, etc.) in the sidebar — they only see this app's screens and a Sign-out. (You, the owner, still see everything.)
- Staff now have Projects and Tickets in their sidebar — they can already work maintenance tickets and record project contributions, so those screens are now reachable for them (Settings stays owner/admin-only).
- Pending sign-ups now live in the Tenants list, not Settings — they appear at the top of Tenants with Approve/Reject. Settings keeps the on/off toggle and the sign-up link, and both the staff and tenant links now have a Copy button.
- Fixed: the tenant dashboard showed amounts in dollars. Charges, balances, and payments in the tenant's portal now show the Naira sign (₦) by default, matching the rest of the app.
- Set your currency symbol once, see it everywhere. Settings → Business Info now has a Currency Symbol field (defaults to ₦). Whatever you set shows on every amount across the app — dashboards, reports, printed receipts and invoices, and the tenant portal.
- Fixed: printed receipts/invoices showed dollar signs. The public receipt page was always printing
$ regardless of your currency. It now uses your configured symbol.
- Tenants can pay a charge by bank transfer. When a tenant opens a charge that still has a balance, they now see your business bank account (bank, account name, and account number with a one-tap Copy) right in the charge, with a reminder to use the charge number as the transfer description. Shown only when you've turned on bank details in Settings.
- Tap a shared project to see its details. In the tenant portal, a shared project now opens a details view with your share, amount paid, what's left, a progress bar, the full payment history, a pay button, and bank-transfer details — instead of just a flat card.
- Fixed: the portal said "You're all paid up" while a project was still owed. The tenant home banner now only shows the green "all paid up" message when both charges and shared-project contributions are fully settled. If charges are clear but a project still has a balance, it shows that amount as due instead.
- Fixed: the property switcher disappeared after deleting a property. If the property you were viewing got deleted, the app could be left pointing at a property that no longer exists — which hid the property switcher (and could show empty lists). It now automatically falls back to your first property, so the switcher always shows and your data loads.
- Tenants can see who else is contributing to a shared project. Opening a shared project in the tenant portal again shows all participants and their status (Paid / Partial / Owing) with how much each has paid, plus an overall "Project funding" card (goal, collected so far, % funded, and number of participants). Restores the peer-visibility view from the old portal.
- Tenants can change their own PIN. Account → Security → Change PIN lets a tenant set a new sign-in PIN (enter current PIN, then a new 4–8 digit one). Restores the self-service PIN change from the old portal.
- The tenant portal is now installable as an app. On a phone, tenants can add your portal to their home screen — it opens full-screen like a native app, branded with your business name and colour. They'll see an "Install app" option in Account (on supported browsers) or can use their browser's "Add to Home Screen". Works from both the sign-in page and the dashboard.
- Tenants pick their property when signing up. If you manage more than one property, the sign-up form now asks the tenant which property they belong to, so they land in the right one instead of always going to your default property. You still approve every request. (With a single property, nothing changes — they're placed there automatically.) You can also share a property-specific sign-up link by adding
?property=ID to the link to pre-select it.
Internal
- Demo data now ships sign-in-ready: 3 staff (Amaka/Tunde/Chioma, PIN 1234) and tenant portal PINs (1234) on most tenants, plus one tenant left without a PIN (to show "Set PIN") and one pending self-registration (to show the approval flow). Existing installs are unaffected — this only changes fresh demo seeds.
- Rebuilt the tenant portal API (
api/portal.php) on the post-pivot app-session + hashed-PIN model; removed the deleted pre-pivot version that referenced columns (portal_pin, user_id, pancho_user_id) that no longer exist. Tenant portal was 404ing on every request before this.
- Staff reach the same admin views/APIs through
/p/{uuid}/propertypro/admin/ via the core _route_public_admin() surface; sign-in routes staff there and tenants to /customer.
- Schema (additive):
customers.pin_plain, self_registered, approval_status (default approved), rejected_reason, archived_at; approval-queue index; unique indexes on payment/project-payment provider references for idempotency.
- Removed dead
create-account / reset-tenant-pin / toggle-tenant-active actions and the broken account UI; added impersonate / stop-impersonate / approve / reject / pending.
- Fixed
get_status_labels() / get_status_colors() which iterated the status-keyed map as a list — they emitted "undefined array key" warnings (which corrupted API JSON when display_errors was on) and returned empty label maps.
app.json: registered the portal API route, added /staff and /signup public routes, populated csrf_exempt_apis, added the self_signup_enabled default, removed the dead /profile nav items, and flipped ready to true.
- Deep-dive fixes: (1)
api/audit.php + the tenant-activity query in api/customers.php selected tm.display_name/u.display_name with no such join (pre-pivot tenant_members leftover) — rewritten to LEFT JOIN staff s ON ('staff:'||s.id)=a.user_id with an Admin/Staff/System fallback. (2) api/staff.php used require_auth() (Pancho-only) so it 401'd for app-side admins on /admin/*; switched to require_permission('staff','manage'). (3) views/ticket-detail.php and views/project-detail.php gated all admin controls behind APP_CONFIG.isAdmin, which the layout never sets — replaced with a can(area,action) helper mirroring require_permission (super-admin + admin-role bypass, else permission matrix), so status/assignment/project controls render for managers again.
- Public invoice payment: the 4 order-payment endpoints now authorize via EITHER a logged-in tenant session OR the order's
share_token, so paying from a shared invoice link (no login) works.
- Core (
core/views/desktop/layout.php): the desktop sidebar's "Your Apps", "Platform", and "Admin" sections are now gated on !$isAppUserActor, so app-side staff/admin on /p/{uuid}/{appId}/admin/* only see the current app's nav (mobile already did this). Affects every app, not just PropertyPro.
- Platform (
extensions/billing/api/apps.php): the premium activate and reactivate paths now seed the per-user DB when it doesn't exist yet, so a fresh premium install (or a deactivate→reinstall whose data was purged) isn't an empty shell. Guarded on file-absence — existing populated DBs are never re-seeded. Affects every premium app.
- Platform (
core/views/{desktop,mobile}/home.php + marketplace.php): added a Manage link to each app on the Home "Your Apps" cards; it deep-links to /myapps?manage={appId}, which the marketplace now honors by auto-opening that app's manage sheet (Open / Cancel App). Affects all apps.
api/entities.php: create now calls on_entity_created() to clone the price list from the first property; added a delete action (admin-only, keeps ≥1 property) that cascade-deletes the property and all its data in FK-safe order inside a transaction, removing uploaded files after commit. views/location-select.php got a per-property delete (trash) button.
api/customers.php: the duplicate-phone check in create/update is now global (not per-property) and both writes are wrapped in try/catch, so a phone already used under another property returns a friendly 409 instead of a raw UNIQUE constraint failed: customers.phone error (phone is globally unique because the tenant portal logs in by phone).
- Currency symbol: the tenant dashboard (
views/public/customer.php) ran on a public route with no Pancho session, so its currency_symbol-fallback compared get_app_currency() (which returns a symbol) against the code 'ngn' — always falling through to $. Now defaults to ₦ when the setting is unset, matching views/public/invoice.php. Also seeded currency_symbol = ₦ and self_signup_enabled = 1 into seeds/demo.sql + seeds/fresh.sql (parity with DryCleanPro) so fresh installs carry the setting. Existing installs are covered by the runtime fallback (no migration needed); admin money was already correct via PS.formatPrice on the owner's currency code.
- Self-signup property placement:
api/portal/register previously hard-assigned every self-registered tenant to self_signup_default_entity_id (or 1). It now reads entity_id from the form, validates it exists in entities, and requires a choice when the install has more than one property (400 "Please choose your property."); a single-property install still auto-assigns, and a missing/stale default falls back to the first property. views/public/signup.php renders a property <select> (hidden single-input when only one property), supports ?property=ID pre-selection, and validates client-side. The admin sign-up notification now names the property.
- Universal currency symbol: added
pp_currency_symbol() / pp_money() to helpers.php (single source of truth, defaults to ₦). views/public/invoice.php was passing the symbol to core format_price(), which expects a currency code and silently prints $ for anything that isn't 'ngn' — that's why every printed receipt showed dollars; it now formats via pp_money(). Added a Currency Symbol input to Business Info settings (saveBusinessInfo persists currency_symbol and reloads). Core PS.formatPrice (core/assets/js/app.js) now prefers APP_CONFIG.currencySymbol when the caller passes no explicit currency code, so the admin's symbol applies across all app views — backward-compatible (the layout already defaulted currencySymbol to match the currency code, so apps without a custom symbol are unaffected). Affects all apps.
- Tenant charge sheet bank transfer:
views/public/customer.php adds DATA.bank (only when bank_account_enabled and an account number exist) and a bankPanel(ref) block + copyText() helper in the charge and project sheets.
- Tenant project details:
DATA.projects[].contributions now includes each project_payments row (amount, paid_at, method); the home project cards are clickable (openProject(assignmentId)) opening a details sheet with progress, payment history, pay button, and bank transfer.
- Tenant project peer visibility:
DATA.projects[].peers (name, unit, assigned, paid, remaining, is_me; customer ids stripped before serialization) and total_collected + total_amount added server-side in customer.php; openProject() renders a "Project funding" overall card and a "Participants" roster with Paid/Partial/Owing badges. Mirrors the pre-pivot portal's peer view.
- Tenant
change-pin (api/portal.php): app-session + per-app-CSRF action; verifies the old PIN with verify_pin(), writes pin_hash + keeps pin_plain in lockstep. Account tab gained a Security section with a Change PIN sheet. (The old portal's activity notification-log tab was not migrated — redundant with the Messages tab.)
- PWA:
core/index.php _serve_public_pwa_asset() serves a per-business manifest.webmanifest, a minimal network-first sw.js, and an accent-tinted icon.svg from /p/{uuid}/{appId}/ (SW scope = that path, no special header needed). customer.php + portal.php link the manifest, add apple/mobile web-app meta + apple-touch-icon, register the service worker, and the dashboard Account tab captures beforeinstallprompt to drive an "Install app" row. SVG icons (no GD on the box); start_url/scope are the tenant portal. The manifest/sw/icon routes are generic in core — any app's public portal can opt in by linking them.
- PWA generalized for all apps: extracted a shared
pwa_portal_head() helper (core/lib/app-context.php); the core route now (a) skips apps that ship their own manifest.webmanifest/sw.js route so EstateMax/ServicePro keep their hand-tuned PWAs, (b) reads pwa.start_route from app.json (default customer), and (c) honours an optional ?slug= so per-entity apps (formbase) install scoped to one entity. Wired DryCleanPro, LogisticsRoute, and FormBase to the helper.
- Tenant home hero: the "all paid up" green state is gated on
DATA.outstanding === 0 && projectsDue === 0 (projectsDue summed from PROJECTS[].remaining); a charges-clear-but-project-owed state shows a dedicated "Project contribution due" hero. DATA.outstanding still only counts charges (orders) — projects are summed client-side.
- Core (
core/lib/app-context.php): get_active_entity_id() now validates the session's app_entity_id against the entities table and self-heals to the first remaining entity (persisting it) when the stored id is unset or points at a deleted entity. Previously it returned the raw session value, so deleting the active property left get_active_entity() returning null — hiding the desktop + mobile entity switcher and silently filtering every query by a non-existent id. Result cached per request; degrades safely (raw session value / 1) if no entities table or DB context. Affects all multi-entity apps.
- Settings → Tenant Sign-ups no longer renders the pending list; pending tenants moved to the Tenants grid (
views/customers.php). The staff sign-in link is now a copyable field like the sign-up link.
views/project-detail.php: removed a stray + '</p>' : '') fragment in the payment-history render that was a JS SyntaxError (Unexpected token ':'), which broke the whole page script so loadProject() never ran. Verified every PropertyPro view's inline JS now parses (Node new Function() check on rendered HTML — php -l and curl can't catch client-side JS errors).
- Schema: dropped the redundant
ALTER TABLE customers ADD COLUMN … lines (pin_plain/self_registered/approval_status/rejected_reason/archived_at) — already declared in the CREATE TABLE, so they only re-added existing columns and tripped fresh-install schema load. The lazy migrator still backfills existing installs.
[2026-05-16]
- Fixed "Add staff" not submitting. The Add-staff form was opening but its fields and button were silently unclickable. The form is now fully interactive.
- Re-adding a revoked staff member with the same phone now works. Previously the database's unique-phone rule would block it with an opaque error; now the original record is revived with the new name, role, and PIN.
- Fixed wrong total in the SMS / push notification sent when a new charge is created. A leftover reference to the removed delivery-fee field meant the broadcast total could come out as zero (or trigger a PHP warning). The new-charge notification now shows the correct amount.
- Fixed SMS sending crash. Sending an SMS via Twilio referenced a missing config file (
config/twilio.php) and an undefined function, so every SMS notification (new charge, payment received, due-soon, overdue) would crash silently instead of sending. Twilio sending now works inline without an external config.
- Clearer feedback on "Change Status" for paid/cancelled charges. Tapping Change Status on a charge that's already paid or cancelled used to do nothing visible. Now a toast explains that no further status changes are available.
Internal
- Removed stale
'DryCleanPro' copy-paste artifact in the staff/tenant login page header and fallback business name.
[2026-05-15]
Internal
- Fixed the per-app uninstall hook signature (
int $userId → string $userId) so order- and customer-attachment cleanup actually fires when an admin or cron purges the install.
[2026-05-14]
- Added a Watch/Read tab on the tutorial page — every lesson now has a written version alongside the video for users who prefer to read.
[2026-05-11]
- Pancho is no longer involved in your team's access. The "Staff & Invites" card in Settings is gone, along with the Pancho invite flow (phone invites, broadcast links, approval queue). Pancho now authenticates only you — the install owner.
- Coming next: an in-app Staff section where you add team members directly and they sign in with phone + PIN (no Pancho account needed). DryCleanPro has the reference build; the same pattern will land here next.
Internal
- All
require_tenant_member() / require_tenant_admin() gates rewritten to require_auth() as a placeholder. apps/propertypro/api/members.php and the members api-route removed.
- Schema:
tenant_members / tenant_invites CREATE TABLE statements stripped (comments remain). Demo seed no longer inserts into tenant_members. identity block removed from app.json.
[2026-05-09]
- Public URLs now show your custom domain. Settings pages and shareable-link UIs that used to render
mypancho.com/p/{uuid}/propertypro/... now render https://your-custom-domain.com/... whenever you've connected a domain to PropertyPro on /account/domains. Falls back to the platform URL if no domain is connected.
[2026-05-07]
- Added a Staff & Invites section in Settings (admin only). Two ready-to-share invite links — one for staff, one for customers — plus an "Invite by phone" form. Copy a link, send it on WhatsApp/SMS, and the recipient signs up to Pancho once and lands here with the right role automatically. A "Rotate link" button under each one invalidates a leaked link.
Internal
- Wired up the canonical members API via a one-line shim at
api/members.php plus a members entry in app.json api_routes. The Settings card markup is sourced from the shared core/views/partials/staff-invites-card.php partial.
[2026-05-01]
- Added a tutorial video to the marketplace info card and a shareable
/video/propertypro page. Apps that ship more than one video get a playlist on that page.
[2026-04-30]
- Removed the duplicate "Charge Type — One-time / Monthly" picker from step 3 of the New Charge wizard. It was a leftover from the dry-cleaning ancestor (it was actually a Pickup/Delivery toggle rebranded with labels) and added nothing — the Cycle dropdown in the Billing Period card already captures whether a charge is one-time, weekly, monthly, quarterly, or yearly, and that's the value surfaced on the charge list, charge detail, public invoice, and tenant portal. Step 3 is now noticeably shorter and cleaner.
- Fixed "Tenant not found." error in the Messages & Notes section on Femi Adeyemi's charge — the demo seed was placing Femi in Palm Court Villas while his charge sat in Sunrise Apartments, so messaging him from his own charge returned a 404. Femi now correctly belongs to Sunrise Apartments (matching his Park Avenue address). New installs pick this up automatically; existing installs need a reseed.
Internal
- Dropped
delivery_type, delivery_fee, delivery_notes from orders (schema, demo + fresh seeds, all order-related queries in api/orders.php, api/portal.php, api/customers.php, api/dashboard.php, api/payments.php, api/reports.php, helpers.php, and the views/order-detail.php / views/public/invoice.php / views/public/portal.php consumers). Pre-launch — no back-compat preserved.
- Removed
pickup_or_delivery, pickup, delivery, delivery_fee label overrides from app.json and seeds/*.sql since nothing reads them anymore.
[2026-04-29]
- Fixed "Invalid status." error when changing a charge's status — the status filter and the Change Status form now accept all configured statuses (Sent, Due, Overdue, Paid, etc.), not just Cancelled.
- Fixed the tenant detail screen — opening a tenant was failing with a server error because the customer query referenced columns that were removed during the identity migration. The screen now loads tenant info, recent rent charges, projects, tickets, and notifications again.
Internal
- Restored
customers.portal_pin (with a guarded ALTER) since the public tenant portal still authenticates against it; the migration dropped the column without rewiring portal auth.
- Replaced two stale
c.user_id references with c.pancho_user_id in api/customers.php (case get, case delete).
[2026-04-21]
Internal
- Removed
ensure_platform_admin_in_app_db() from helpers.php — it was querying an app-local users table that was retired during the identity migration, so it threw (silently caught) on every page load. The modern tenant_members table + _per_user_app_bootstrap_owner() hook replaces its purpose entirely.
[2026-04-20]
- Fixed broken share-invoice URL — the share-receipt button was silently losing the tenant id (Pancho UUID was cast to an integer, producing
/p/0/…). Share links now resolve correctly.
- Demo install now loads sample data cleanly — the seed file was still referencing the old
users table that was retired during the identity migration, so Try Demo was erroring out on install.
Internal
- Activity Log API now joins
audit_log.user_id to tenant_members.pancho_user_id (the old users table was dropped during the identity migration). No user-visible change; the page just stops 500ing when opened.
settings-sections.php portal URL now url-encodes the UUID segment.
seeds/demo.sql — staff/owner/tenant rows moved from users to tenant_members; customers.user_id/portal_pin columns replaced with customers.pancho_user_id.
[2026-04-17]
- Removed the floating action button (+) from the ticket detail screen — it was overlapping the message compose area
- Fixed the tenant portal Pay button disappearing when Stripe is the active provider — the check now only requires keys to be saved (the separate Enable toggle is no longer a gate, so admins can't forget to flip it). Same cleanup for PayStack
- Stripe card modal now shows a "Loading secure payment form…" indicator while Stripe Elements initialise (they can take several seconds on slower networks), and the Pay button stays disabled until the form is ready. Real Stripe load errors now surface instead of silently spinning
- Tenant portal now accepts phone numbers in any common format — US with or without "+1" / "1", Nigerian with or without "+234" / "234" / leading "0". Tenants no longer need the exact format to log in
- Tenant detail page now shows the tenant's maintenance tickets (subject, status, date) after the Projects section — tapping one opens the ticket
- Fixed the PayStack / Stripe settings section in dark mode — the "Get your API keys" info box, the processing-fee panel, and the "Default: 1.5% + ₦100..." note now use theme-aware colors instead of hard-coded light-mode pastels
- Tenant detail page now shows a "Recent Activity" section with the last 10 audit entries for that tenant (charge created, status change, payment, ticket update, etc.)
- Fixed ticket message attachments (photos/PDFs) not showing up in the chat on both admin and tenant portal — image URL was being resolved against the app route and 404'ing; now loads from the web root
- Fixed tenant search not filtering when typing in the New Ticket sheet
- Fixed submitting a ticket (admin or tenant) crashing with "call to undefined function generate_ticket_number"
- Fixed ticket detail crashing with "no such column: tm.attachment_path" — ticket messages can now include file attachments
- Fixed tenant portal: entering a phone number not in the system no longer leaves the button stuck on "Checking..."
- Fixed tenant portal: demo now includes ready-to-use tenant accounts — any demo tenant can log in with PIN 1234
- Updated subscription price to $47/month (USD) and ₦25,000/month (NGN); setup fee is now $497 (USD) and ₦450,000 (NGN)
- Fixed Print Receipt showing a 500 error on the live server — removed stale bootstrap includes that the router already handles, which were crashing when the referenced config files didn't exist on the server
- Fixed Print Receipt still 404-ing — the public-route dispatcher was building the wrong file path ("views/public/public/invoice.php") and failing to open the actual receipt page
- Fixed Delete Charge failing with an integrity-constraint error — the delete now also cleans up attached files and detaches customer notes, so no more foreign-key complaints
- Rewrote the marketplace description, benefits, and features to be clearer, more outcome-focused, and work for landlords or property managers anywhere (not tied to a specific region)
- Rewrote the PropertyPro client proposal (marketing/proposal.md) to cover more use cases — landlords, property managers, caretakers, hostel operators, real estate companies — with a clearer problem-and-solution narrative and real benefits instead of only features
- Restored the "Promote" tab in the app marketplace on mobile and desktop — every signed-in account now gets an affiliate link automatically, so the tab always shows up when the affiliates extension is on
- The client proposal now shows up in the Promote tab alongside Email, WhatsApp, Facebook, etc. with a Copy button — affiliates can grab the ready-to-send pitch in one tap (works for every app's
marketing/proposal.md, not just PropertyPro)
- Fixed Owners page showing "Access Denied" for the account owner — the list, add, edit, activate, PIN reset and delete actions all work again
- Fixed Projects and Tickets admin actions (create/edit/delete) that were also 403-ing for the account owner
- Fixed narrow message / note / status composer boxes across the app — they now fill the sheet width on mobile
- Fixed the discount "Why?" field overflowing the edit charge sheet on small screens
- Fixed uploaded photos/documents not showing a preview in the charge detail page
- Fixed "Print Receipt" on a charge opening a 404 page — it now opens the public receipt cleanly
- Fixed home page crash ("no such table: orders") caused by a bad migration step — home, charges and dashboard now load on fresh and existing installs
- Fixed tenant detail page crash ("no such column: c.user_id") — tenant profile, recent charges and outstanding balance now load correctly
- Fixed Activity Log link on the home page — tapping it now opens a filterable, paginated audit trail instead of a 404
- Fixed demo re-install crashing with "FOREIGN KEY / UNIQUE constraint failed" when a previous install was still in grace period
- Added Activity Log screen: searches by details, filters by action type, newest-first, admin-only
Internal
- Swapped
require_admin() for require_permission($area, $action) across api/owners.php, api/projects.php, api/tickets.php so the platform-user short-circuit (role='admin') applies to in-app admin actions
- Added
owners entry to the app.json permissions matrix
- Changed
.form-input in core/assets/css/core.css to width: 100%; max-width: 100%; box-sizing: border-box — covers every sheet composer that had narrow HTML-default widths
- Fixed attachment preview URL construction in
views/order-detail.php (dropped stale basePath prefix — uploads live at web root)
- Rewrote
orders.share-token URL to the public-route pattern /p/{userId}/{appId}/invoice?token=…
- Fixed
require_once depth in views/public/invoice.php and views/public/portal.php — corrected from 3 to 4 levels up so the bootstrap file resolves on the live server
- Added
user_id (REFERENCES users) and portal_pin columns to the customers table in schema.sql; the framework's lazy migrator adds them on existing installs
- Added
/audit route, views/audit.php, and api/audit.php (admin-only)
- Rewrote
seeds/demo.sql and seeds/fresh.sql to use INSERT OR IGNORE for fixed-ID rows so re-seeding an existing DB is safe (grace-period case)
[2026-04-16]
- Added Stripe as a second payment provider alongside PayStack — admin picks PayStack (default) or Stripe from Settings
- Tenants see no provider branding — same "Pay" button regardless of provider
- Added Stripe keys and fee configuration in Settings (mirrors existing PayStack section)
- Added "Special Projects" toggle in Settings to enable/disable the Projects feature at runtime
- Added "Shareable Links" section in Settings with copy-to-clipboard buttons for tenant portal and invoice URLs
- Project contributions now support both PayStack and Stripe payment flows
- Simplified payment provider setting — removed confusing Auto mode; just PayStack or Stripe
- Fixed Charges page crash (missing billing columns in database)
- Fixed Owners page not loading (missing active/must_change_pin columns)
- Fixed Projects nav item still showing when the feature is disabled
- Fixed search bar styling on Charges, Tenants, and Tickets pages
- Fixed button styling on Tenants and Tickets pages to match platform standard
- Fixed all bottom sheets (New Project, New Ticket, Add Owner, Record Payment, etc.) — now slide up correctly with overlay and drag-to-close
- Fixed recording project payments (and other admin actions) failing with foreign key error
Internal
- Platform admin is now auto-synced into the app's users table on first access — prevents FK violations on existing databases without requiring a DB wipe
- Created
lib/payments.php — centralised provider resolution and payment verification for both providers
- Added
stripe_payment_intent_id column to payments and project_payments tables
- Added
billing_year and billing_cycle columns to orders table
- Added
active and must_change_pin columns to users table
- Nav rendering now respects the
feature key on nav items (generic fix for all apps)
- Fixed dangling
require_once for non-existent config/paystack.php
- Replaced hardcoded
'NGN' currency with dynamic currency from settings
is_feature_enabled() now respects per-app features_* overrides in the settings table
[Baseline] — 2026-04-16
- Existing feature set captured as the baseline. Subsequent changes will be prepended above this entry.