=== PaintOps ===
Contributors: paintops, freemius
Tags: painting, contractor, estimates, scheduling, invoicing
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.0.275
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Run your painting business from WordPress: quotes to invoices, crews, scheduling, customers, photos, profit reports, optional AI and card payments.

== Description ==

**PaintOps turns your WordPress site into the back office for your painting business.** Build professional quotes, send them to customers who can view, e-sign, and pay a deposit online, schedule crews on a calendar, track every job from lead to invoice, and, on Pro, see your real profit per job, per crew, and per worker.

It was built by a painter, for painters. The quote builder understands painting scope: interior/exterior components, standard inclusions and exclusions, and the disclaimer language that keeps you out of disputes. Your data lives in your own WordPress database, not locked inside someone else's SaaS.

= Quote to invoice, start to finish =

* **Quotes, estimates & bids**: a scope-aware builder with reusable inclusions/exclusions, itemized or lump-sum pricing, printable output, and email delivery.
* **A professional customer page**: your customer opens a clean, branded page (your logo, your terms & conditions) and accepts by typing their name, an e-signature-grade record with timestamp. They can also decline with a reason, or ask a question, and you're notified either way.
* **Acceptance converts the quote to a job automatically**, whether they paid a deposit online or you shook hands, and a "Mark as Won" button covers the phone call.
* **Invoices**: numbered invoices for the balance (full or partial), emailed or shared by link, payable online (Pro + Stripe) or marked paid for check/cash, with the payment logged so balances always agree.
* **Receivables at a glance**: unpaid invoices surface on your dashboard with amounts and age.

= Running the work =

* **Scheduling calendar** with crew filtering, worker badges, time-off, and a subscribe link for Google/Apple/Outlook. Add your own recurring holiday labels.
* **Crews**: group your painters into named, colored crews; staff a job with one tap and filter the calendar and reports by crew.
* **Job board / pipeline**: Quote → Active → Completed, plus a lost-leads drawer with the customer's decline reason so you can revive the ones worth re-working.
* **A real app for your painters**: install-to-home-screen (PWA), a "My Jobs" home, one-tap directions (Google/Apple/Waze), per-job check-in/out that works offline and syncs, photo/receipt/note logging, a "My Hours" view of their own time and pay, and a "Mark Job Complete" button that notifies the office. Workers see only what their permissions allow, never your numbers.
* **Customers**: a directory with contact details, lifetime value, job history, and **multiple service addresses per customer**, built for landlords, realtors, property managers, and GCs whose one account means a dozen properties.
* **Before/After photos** with camera alignment overlay and a compare slider.
* **Trade calculators, paint-color tracking, and resource links.**
* **A choice of visual themes** (light, dark, industrial, and more) with a modern app-style navigation bar on desktop and phone.

= What Pro adds =

* **Job financials & profitability**: costs, materials, labor hours; gross, expenses, profit, and margin per job; a Profits dashboard with profit-by-classification, profit-per-hour rankings, and **profit by crew and by worker** (allocated by logged hours).
* **Expenses & receipts** against any job, plus business-level and vehicle expense tracking for tax time.
* **Online payments**: deposits on quote acceptance and invoice balances through Stripe Checkout, recorded automatically.
* **AI tools (bring your own OpenAI key)**: estimate dimensions, gallons, and a Description of Work from a photo; scan receipts to auto-fill expenses; get feedback on or a rewrite of your quote wording.

Pro is a paid upgrade handled through Freemius. The free plugin is fully functional on its own; Pro is optional.

== External services ==

This plugin can connect to the following third-party services. Some are optional and only run when you enable and configure the corresponding feature.

**OpenAI (Pro AI features, optional).** When you use an AI feature (Photo Estimate, Receipt Scan, Description-of-Work Feedback or Rewrite), the plugin sends the relevant input, the photo you choose to upload and/or the quote text you are working on, to the OpenAI API using **your own** OpenAI API key, and receives the generated result. No data is sent unless you click one of these buttons. PaintOps does not store the photo. Requests go to https://api.openai.com. See OpenAI's Terms (https://openai.com/policies/terms-of-use) and Privacy Policy (https://openai.com/policies/privacy-policy).

**Stripe (Pro online payments, optional).** If you enable card payments and a customer pays online, the plugin creates a Stripe Checkout session and verifies Stripe's webhook using your own Stripe keys. Order/amount details are exchanged with Stripe to process the payment. Requests go to https://api.stripe.com. See Stripe's Services Agreement (https://stripe.com/legal/ssa) and Privacy Policy (https://stripe.com/privacy).

**Google Maps Platform (optional).** If you add a Google Maps API key, address fields use Google Places/Geocoding for autocomplete and validation, and job locations may be shown on an embedded map. The address text you type is sent to Google to return suggestions. Requests go to https://maps.googleapis.com and related Google Maps endpoints. See Google's Terms (https://cloud.google.com/maps-platform/terms) and Privacy Policy (https://policies.google.com/privacy). (One-tap turn-by-turn Directions for your crew use keyless map links and need no API key.)

**YouTube and Vimeo (Resources page video embeds, optional).** The Resources page (staff-facing, inside your admin area) lets you save links to training videos, and seeds a starter list of publicly available painting tutorials on YouTube. When a staff member clicks a saved YouTube or Vimeo link's play button, the video is embedded on the page via the provider's standard embed player (www.youtube.com / player.vimeo.com), which loads content from that provider and is subject to its terms; no data is sent until the play button is clicked, and you can delete any or all saved links. See YouTube's Terms (https://www.youtube.com/t/terms) and Google's Privacy Policy (https://policies.google.com/privacy), and Vimeo's Terms (https://vimeo.com/terms) and Privacy Policy (https://vimeo.com/privacy).

**Freemius (licensing & updates).** PaintOps uses the Freemius SDK to manage Pro licenses, deliver Pro updates, and (optionally) collect anonymous usage diagnostics you can opt out of. Site and license information is exchanged with Freemius. See Freemius's Terms (https://freemius.com/terms/) and Privacy Policy (https://freemius.com/privacy/).

You control every one of these by whether you enter the corresponding API key and enable the feature. With no keys entered, PaintOps runs entirely on your own site.

== Installation ==

1. Install PaintOps from the Plugins screen (Plugins → Add New → search "PaintOps"), or upload the plugin ZIP under Plugins → Add New → Upload Plugin. Activate it.
2. Create a WordPress Page and add the shortcode `[paintops_app]`. This one page is your whole PaintOps app; it routes internally.
3. Go to **PaintOps → Settings** and set your Company Profile (business name, logo, country, currency), and review the Setup Health panel.
4. (Optional) Add team members under **PaintOps → Painters/Helpers**, set their permissions, and group them into Crews.
5. (Optional) Add your Quote Terms & Conditions under Business Settings so customers see and sign against them.
6. (Optional, Pro) Under **Settings → AI Settings** connect OpenAI, under **Stripe Payments** add your Stripe secret key AND webhook signing secret (both are required before online payment buttons appear), and add a Google Maps API key for address autocomplete.

== Frequently Asked Questions ==

= Do I need a paid account to use it? =
No. The free plugin runs entirely on your own WordPress site and lets you quote, schedule, and manage unlimited jobs. Pro is an optional upgrade.

= What happens to my data if I deactivate or delete the plugin? =
Nothing is deleted. Your jobs, quotes, customers, payments, hours, and photos stay in your WordPress database, so nothing is lost if you deactivate temporarily or reinstall later. If you truly want the data gone, delete the plugin's tables and its uploads from your database and media library.

= Does it work without OpenAI, Stripe, or Google Maps? =
Yes. Those are optional integrations. If you don't enter their keys, the related features simply stay off and everything else works, including keyless turn-by-turn directions for your crew.

= Can my customers really sign a quote online? =
They accept by typing their full name, which is stored with a timestamp and IP address alongside your Terms & Conditions, a solid acceptance record. They can also decline with a reason, and change their minds later.

= Who pays for the AI usage? =
You do, through your own OpenAI account and key, typically a fraction of a cent to a few cents per action. PaintOps adds per-month usage caps you control so costs stay predictable.

= Can my painters log in without seeing my numbers? =
Yes. Painters/Helpers get a dedicated role with opt-in permissions and their own app view: My Jobs, My Hours (their own time and pay only), directions, check-in/out, photos and receipts. Financial figures are owner-level and hidden from workers unless you explicitly grant access.

= Does it work in the field with bad reception? =
The app installs to a phone's home screen, and Time Clock check-in/out keeps working offline and syncs when a connection returns.

= Where is my data stored? =
In your own WordPress database. PaintOps doesn't host your business data on an external server.

== Screenshots ==

1. The quote/estimate builder with scope components and inclusions/exclusions.
2. The customer's accept page: branded, e-signed, with online deposit.
3. The scheduling calendar with crews, worker badges, and holiday labels.
4. A painter's phone view: My Jobs, directions, and offline check-in.
5. The Profits dashboard (Pro) with profit by crew and worker.
6. An invoice with online payment and mark-as-paid.
7. Settings: Company Profile, Appearance themes, Stripe, and AI.

== Changelog ==

= 1.0.275 =
* Fixed: many in-app links (Create Quote in More Actions, calendar month arrows, back links, save banners, and others) could land on the site's blog page on sites using plain permalinks. Every link now carries the app page with it.
* Fixed: the Jobs page rows squeezed the job title to almost nothing inside themes with a normal content column. Rows now adapt to the width of their own card.
* Fixed: a false "You are offline" banner and an unstyled Install PaintOps button could appear at the bottom of some screens.
* Fixed: new quotes printed the paint color confirmation paragraph twice in the Exclusions. The duplicate starter clause is removed and already-saved notes are cleaned up automatically on update.
* Improved: if the quote email fails to send (common on hosting without a mail service), the screen now explains why and shows the customer's accept link to copy and send yourself.
* Improved: after saving a quote you now get Open, Email It to the Customer, and Back to the Job actions instead of a bare one-line notice.
* Fixed: the customer accept page now always shows the accept-by date, even before Quote Terms are saved.
* Changed: the Complete / Not Complete pill on the job view now submits a form instead of following a link, so browser prefetching can never flip a job's status.
* Fixed: on the free plan, the Collect link on finished unpaid jobs goes to the job's Mark as Paid action instead of an upgrade screen. Pro still goes straight to the Payments log.
* Fixed: quote emails and review request emails no longer show blank spots before the business profile is filled in. The signature and business name fall back to the site title.
* Changed: the itemize section on the quote form starts collapsed.

= 1.0.274 =
* The Complete / Not Complete toggle on the job view now redirects to a clean URL after saving, so browser refresh or Back can never flip it again, and the app tells page-caching plugins never to cache its screens for logged-in users (a cached copy could show a job's old status after a change).
* The Jobs page's Collect link on unpaid finished jobs now opens the job's Financials page instead of the job view.

= 1.0.273 =
* The Jobs page has a new default view: clean rows grouped into Active Jobs, Quotes Out, and Wrapping Up, in the same order every day, with crew, dates, amounts, quote viewed/quiet states, and one-click follow-ups. The classic three-column board is still there via the Switch to Board view link at the bottom of the page.

= 1.0.272 =
* A fresh coat for the whole app: modern type, softer corners, and cleaner card surfaces across every screen and every theme. The Seaside theme also gets a soft page background so cards stand out, and its secondary text regains a readable muted tone.

= 1.0.271 =
* Pro: job receipt coversheets opened from a Business-page tax year now limit their expense lines and attached receipts to that year, so year-straddling jobs print a clean per-year filing bundle (undated entries stay included, and whole-job Profit Summary figures are labeled as such).

= 1.0.270 =
* Quotes now carry a validity date (default 30 days, configurable under Business Settings, 0 disables). The valid-until date prints at the bottom of the quote, and once it passes, the customer accept page stops taking online acceptances and invites the customer to request updated pricing. Already-accepted quotes are never affected.

= 1.0.269 =
* Pro: Google review requests. Set your Google review link and a default email under Settings, and the Jobs page suggests completed jobs whose customers haven't been asked yet, with a preview-and-edit compose screen, one-click dismiss, and every send logged to the job's communications.

= 1.0.268 =
* Pro: after upgrading from the free version, the leftover deactivated free copy is now removed automatically (with a confirmation notice). No data is affected.

= 1.0.267 =
* Jobs dashboard: quotes you know are dead can now be archived straight from Needs Your Attention; Archive marks the quote Not Won (same as the job board action, reversible there) and Not Won quotes no longer appear in the stale-quotes list.

= 1.0.266 =
* Jobs dashboard: sending a follow-up email or tapping Text Follow-Up now clears the quote from Needs Your Attention (any logged communication inside the stale window counts as activity, and the quote resurfaces if the customer stays quiet past it). The dashboard Text Follow-Up tap is also logged to the job's communications like everywhere else.
* Pro: new one-time welcome screen shown after activation with what's new and first setup steps.

= 1.0.265 =
* Time Off: the Year/Month/Day row now carries its 3-column layout inline, so it renders identically on every site regardless of stylesheet load order (it previously collided with the 4-column address-row definition and could render 4-up on some themes).

= 1.0.264 =
* Theme compatibility: the plugin's own UI elements now pin box-sizing: border-box, so buttons and multi-column forms render as designed on themes that lack a global border-box reset. No visual change on themes that already have one.

= 1.0.263 =
* Reverted the 1.0.261/1.0.262 app-wide button sizing changes; the compact height now applies only to the job board's + New Request button.

= 1.0.262 =
* App buttons: fixed the inflated button height on themes without a border-box reset (box-sizing now set explicitly) and tightened the desktop button profile to about 36px; touch devices keep the full 44px tap target.

= 1.0.261 =
* Job board: the compact action buttons inside card drawers (Reopen as Job, stage moves) no longer inherit the app-wide 44px touch-target height on desktop; touch devices keep the full 44px tap target.

= 1.0.260 =
* Security review round 3: pdf.js is now bundled with the plugin (assets/vendor/pdfjs) instead of loaded from cdnjs; the unused media.php core include was removed; nonce verification is now the first standalone statement in the reminder AJAX handler and explicit inside the calculator readers; remaining sanitize sites gained wp_unslash; token-authenticated public endpoints and read-only display filters are documented at each read site.

= 1.0.259 =
* Code quality: the Estimates, Calendar, Time Off, and Business Checkup screen files are now wrapped in named render functions, making their variables explicitly function-scoped and clearing all 575 variable-prefix warnings from the wordpress.org hosted Plugin Check.
* Code quality: documented ignores on the field-mapping loops whose keys come from fixed schema or whitelist lists.

= 1.0.258 =
* Admin notices from the licensing SDK now use a plainer tone ("Success! Your plan was successfully upgraded." instead of "Yee-haw!").

= 1.0.257 =
* Final Plugin Check cleanup: the five remaining tool warnings (array-form prepare arguments miscounted, and two documented meta-key lookups) now carry scoped annotations the checker honors. No functional changes.

= 1.0.256 =
* The Calendar screen now prints its own "Calendar" heading. It was the one screen that relied on the theme's page title, which PaintOps hides on app pages, so on some themes the calendar had no visible title.

= 1.0.255 =
* Fix: on themes whose navigation menu or page list includes the PaintOps app page, the menu label wrongly showed the current app screen name (for example "Calendar") instead of the page's own name. The dynamic title now applies only to the page's actual title areas.

= 1.0.254 =
* Plugin Check zero-error pass: the plugin header's "Requires at least" now matches the readme at 6.2 (this also clears every false %i placeholder error), all escaped output calls wp_kses() by its literal name so scanners recognize the escaping, the Business Checkup queries carry their fixed SQL conditions inline, the calendar's crew filter is part of one static prepared query, and the remaining tool false positives carry precisely scoped justifications.

= 1.0.253 =
* Escaping conformance polish on a handful of remaining literal outputs ahead of the WordPress.org re-review. No functional changes.

= 1.0.252 =
* Job board: Call / Text / Email on lost leads are now one tidy segmented control with icons, and History steps back visually. Invoice page: the action bar is decluttered (Print, Email, Mark as Paid stay up front; Copy customer link, the AI reminder, and the pay link live under More Actions), the AI-drafted reminder can now be sent directly from the page after review (logged to the communications history), and "Email to customer" asks before its immediate send. Quote builder: the AI description tools no longer vanish on short text; they show disabled with a note that they unlock at 200 characters.

= 1.0.251 =
* Audit follow-ups: settings saves no longer strip literal backslashes from typed text (double-unslash regression in 1.0.249); the van icon and link-arrow SVGs keep their shading and rounded strokes through output escaping; photo caption line-clamping is preserved; Stripe key diagnostics render their formatting again; several links now use the exact right escaper for their context; the calculator nonce is scrubbed from the address bar after a Calculate. Business Checkup items gained one-click action links (Create invoice, Log hours) and invoice photos now display two-up at half width.

= 1.0.250 =
* Fixes from live testing of 1.0.249: HTML badges and notices that rendered as raw markup (job view title badges, financials accordion summaries, Estimate vs Actual variance column, business/vehicle notices, worker hour dropdowns) now escape through the correct HTML-aware helper; sms: and webcal: links survive output escaping; and a latent fatal on the Business Checkup page (only triggered once a completed job had a balance due) is fixed.

= 1.0.249 =
* Security hardening for the WordPress.org plugin review. Every database query now goes through $wpdb->prepare() with %i table-identifier placeholders (WordPress 6.2+, "Requires at least" raised to 6.2 accordingly). All plugin-built HTML is escaped at the point of output through a central wp_kses allow-list; the settings pages sanitize every submitted array at intake and uploaded files are sanitized before the upload handler. The trade-calculator state and the settings deep-open links now carry nonces. The service worker, the customer quote/invoice page styles, the payment-finalizing poll, the printable report styles, and the report PDF renderer all moved out of inline tags into enqueued asset files. The legacy rl_resource post type was renamed to paintops_resource (with an automatic one-time migration of existing entries, including their URL field) and the [resource_links] shortcode is now [paintops_resources]; if you placed the old shortcode in a page, update it. The Freemius bootstrap accessor was renamed from pai_fs() to paintops_fs().

= 1.0.248 =
* Fix (Pro): a stray test clock-in with almost no time on the clock could produce absurd Profit / Hour figures (tens of thousands per hour) on the job financials, reports, and rankings. Rates past a plausibility cap (default $3,000/hour, filterable) now display as a dash instead, such jobs are excluded from the Profit Per Hour rankings, and the AI profit explainer is told the rate is not meaningful rather than being fed the bad number.

= 1.0.247 =
* Fix (Pro AI): the AI rewrite and photo estimator no longer add a "Description of Work" heading on top of their text; the quote already prints that heading itself. The model is instructed to skip it, and any heading that slips through is stripped before the text reaches the editor.

= 1.0.246 =
* Fix: the Description of Work / Inclusions saved from the quote editor lost all its formatting (line breaks, bold, lists) the moment the quote was created, displaying as one run-together paragraph. The editor's rich text is now preserved on save while still stripping anything unsafe. Quotes saved while the bug was live need their description re-entered once; new saves keep formatting.

= 1.0.245 =
* WordPress.org plugin review round 1 response. The free plugin is now fully functional with no locked features: the monthly job/quote cap is removed (unlimited jobs and quotes on free), and all Pro-only code (Stripe payments, AI tools, profit reports, vehicle and business tracking) ships only in the Pro build rather than sitting locked in the free one.
* The "Powered by PaintOps" credit on quotes and invoices is now strictly opt-in for everyone: it stays off unless you turn it on under Business Settings > Quote & Invoice Branding.
* All of the app's CSS and JavaScript now loads through the WordPress enqueue system (as static asset files plus small dynamic fragments) instead of inline style/script tags, per WordPress.org guidelines. Customer-facing standalone documents (public quote/invoice pages, printable reports) keep their self-contained styling by design.
* Documented the Resources page's YouTube/Vimeo video embeds in the readme's External services section.
* Hardened input sanitization (quote overflow line items, time-off form) and output escaping (time-off notices, upload error details) in several spots.

= 1.0.244 =
* When the app is offline and shows a saved copy of a page, it now says so: a banner at the top shows when the copy was saved, warns that anything scheduled since then is not shown, and the page refreshes itself the moment the connection comes back. Before this, an offline snapshot looked exactly like live data, which could read as a job missing from the calendar.

= 1.0.241 =
* Fix: the Painters/Helpers admin table did not fit small screens and could not be used on mobile. It now scrolls horizontally within its own container, matching every other wide admin table in the plugin.
* Fix: a Callback or Punch List visit with no billable amount of its own, since it exists to finish or warranty its parent job rather than stand as its own job, could still show up as an entry in the "Bottom 3 Jobs" profit ranking (Pro). Non-billable callback and punch list visits no longer count as their own ranked job; a billable callback or punch list still does.

= 1.0.240 =
* Fix: a Painter/Helper opening the app's plain web address instead of a specific job link saw "Please log in to access PaintOps" even while already signed in, and had no way in except a job's night-before email link. They now land on their "My Jobs" home (today, upcoming, and anything needing attention), a page that already existed for this but could not be reached from the plain address before.
* A worker's "My Jobs" home now has a Past Jobs list too, every completed job or visit they were on, so there's a way back to an old job for a note, a still-needed receipt, or a callback follow-up.
* Adding a receipt to a job now offers AI scanning first, same as the Vehicle Log: choose "Scan Receipt with AI" and the photo picker opens right away with the scan running as soon as a photo's picked, or choose "Manually Enter" for an empty form. (Pro with AI configured only; unchanged otherwise.)

= 1.0.239 =
* Fix: the offline app cache could get stuck showing a stale "please log in" screen for a job page even after the worker signed back in, because the background cache refresh skipped re-fetching any page it had already saved once. It now always refreshes the saved copy in the background, so offline job pages stay current with the worker's actual signed-in state.

= 1.0.238 =
* Fix: the job Time Clock (check-in/check-out) could display times up to several hours off from the actual time on servers where another plugin changes PHP's default timezone, the "Checked in at" confirmation was correct but the "Checked in since" status line, the recent time-on-this-job list, and the checkout note could all be wrong. All check-in/check-out time displays now render consistently and correctly.

= 1.0.237 =
* Fix: Plugin URI and Author URI headers no longer share the same value (wp.org submission requirement); Author URI now points to the founder's story page.

= 1.0.236 =
* Code review annotation added to the new routing rescue file. No functional changes.

= 1.0.235 =
* All date displays are now immune to other plugins or themes overriding PHP's timezone. Previously, if anything on the site changed PHP's default timezone away from UTC (WordPress pins it to UTC, but some plugins override it), every PaintOps date rendered between midnight and the early morning hours could show the previous day, the Jobs page, calendar, and reports now show the correct site-local date regardless.

= 1.0.234 =
* The Jobs page now computes "today" exactly once per page load using the WordPress site clock, so the Today widget and the job board can never disagree, previously a page rendered while the site's clock crossed midnight could say "nothing scheduled today" above a card marked "started today".

= 1.0.233 =
* Reactivate License buttons and rescued permission errors on an unlicensed Pro install now open the Plugins page, where the Activate License link under PaintOps works in every license state, with a bold pointer added under the button.

= 1.0.232 =
* On a Pro install that is waiting for its license key, the Upgrade to Pro buttons now read Reactivate License and open the license screen directly, matching what the user actually needs to do in that state. The free version's upgrade buttons are unchanged.

= 1.0.231 =
* The "Sorry, you are not allowed to access this page" dead-end is gone: if an Upgrade link lands on a licensing page that does not exist in the site's current license state (most commonly on a Pro install waiting for its license key after a reinstall), PaintOps now catches the error and opens the main PaintOps admin page, where the license screen appears whenever a key is needed.

= 1.0.230 =
* Upgrade buttons now route by license state: on an already-licensed site they open the Account page, and on the Pro build awaiting its license key (for example right after a reinstall) they open the license activation screen. Both cases previously dead-ended on a WordPress permissions message.

= 1.0.229 =
* Release build: everything through the Plugin Check zero, the premium package fix, and the address autocomplete fix. The upgrade-link routing refinements from 1.0.224/1.0.225 are deferred (edge cases around stale pages only) while a build tooling issue is investigated with Freemius.

= 1.0.228 =
* Same upgrade-button routing as 1.0.225, reformulated for the build tooling. No functional changes.

= 1.0.227 =
* Same as 1.0.225 (upgrade buttons route by license state) with the code annotations around that change simplified for the build tooling. No functional changes.

= 1.0.225 =
* On the Pro build without an active license, the Upgrade to Pro buttons now open the license activation screen (where entering your key is the actual upgrade step) instead of a pricing page that does not exist in that state.

= 1.0.224 =
* On a site that already has an active Pro license, any leftover Upgrade link (for example on a page rendered just before the license was activated) now opens the Account page instead of a WordPress "not allowed" error, since the pricing page does not exist once a site is paying.

= 1.0.223 =
* Address autocomplete no longer randomly fails to start on the job and customer forms. The Google Maps script loads asynchronously and could occasionally finish before the page had defined the function it calls back into; the loader is now printed after that function and uses Google's recommended loading=async parameter, so the race cannot happen.

= 1.0.222 =
* Build fix for the premium package: the vehicle and business expense report pages (with their printable coversheet templates and the PDF receipt printing panel) moved from the main plugin file into includes/pro-expense-reports.php. The premium build generator re-emits the main file through a code printer that cannot handle raw HTML template sections inside a premium-only block, which is what produced the empty main file. Include files are packaged verbatim, so the reports are unchanged in behavior on both tiers.

= 1.0.220 =
* Build fix attempt for the premium package: the closing comments on the premium-only blocks in the main file no longer repeat the premium marker text, so the build tool sees exactly one marker per real block. No functional changes.

= 1.0.218 =
* Build fix attempt for the premium package: the Freemius init array is now free of inline comments (one of them contained the exact token the premium build step is documented to remove, and that step regenerates this array). No functional changes.

= 1.0.217 =
* Plugin Check: the last 5 findings closed. The build generator splits compact one-line statements across lines, which pushed three footer outputs and the app-page lookup's meta arguments out from under their review annotations; each annotation now sits directly above the exact line it covers, in a structure the generator preserves. No functional changes.

= 1.0.216 =
* Build fix: the plugin header comment is back at the very top of the main file, ahead of the code review annotations added during the Plugin Check cleanup. The premium build generator requires the header there and was producing an empty main file, which made the premium zip uninstallable ("No valid plugins were found"). No functional changes.

= 1.0.215 =
* New workers are no longer stranded at the door: the "Please log in to access PaintOps" screen now shows a Log In button that takes them to the login form and returns them to the exact page they were opening, including the specific job from a job notification email. The job notification email also mentions what to do if the login screen appears.

= 1.0.214 =
* Plugin Check round two: the 204 findings that survived 1.0.213 are closed. The build tool that produces the free version rewrites statements and relocates end-of-line comments, which silently disabled many of the 1.0.213 review annotations; all annotations in rewritten files now sit on their own line above the code they document, where the build preserves them. More real hardening while at it: pricing-rule numbers, company profile dropdowns, proxy-header reads, and the form replay token are now fully sanitized and unslashed at the source; every URL echoed on admin screens goes through esc_url() at output; month options, crew ids and diagnostic messages are cast or escaped at output.

= 1.0.213 =
* Plugin Check cleanup pass: every remaining finding resolved. Real hardening fixes: nonce values are now sanitized and unslashed before wp_verify_nonce() everywhere, quote titles, supply descriptions and work descriptions are sanitized and unslashed on save (also fixes apostrophes storing as \\' in quotes), payment/invoice tokens, routing parameters and time-off fields are sanitized, job IDs and crew IDs cast to integers in output, Google Earth links escaped with esc_url(). Everything intentionally left as-is (plugin-owned tables via $wpdb, pre-escaped HTML builders, whitelisted dropdown values, integer casts) now carries a written justification for reviewers.

= 1.0.212 =
* Fix: the free plan no longer offers or defaults to "Card deposit online" on the job form (online card deposits ride on Stripe, a Pro feature). Free records default to the cash/check deposit plan; a record already set to the online plan keeps it visible when editing.

= 1.0.211 =
* Improve: the PDF-receipt print panel now has an adjustable "Pages per PDF" cap, default 30 (was a fixed 15), so long multi-receipt PDFs render in full.

= 1.0.210 =
* New (Pro): the Business Financial Report's receipt version can now print PDF receipts in one pass. A floating "Render PDFs for printing" button converts every attached PDF into pages right inside the report, so a single browser Print captures the whole stack, no more opening each PDF by hand. Anything that fails to render keeps its placeholder card and open link.

= 1.0.209 =
* Improve: in Taxes & Filing, the receipt search now sits above the job coversheets, and the Find Duplicates button wears amber so it reads as the problem-hunting tool it is. The Business Financial Report defaults to totals only, the way accountants actually file, with a separate "+ Receipt Images" button for the full audit file; the "Receipts attached" checklist line only prints on that version.

= 1.0.208 =
* Improve: the Business page now uses the same collapsible sections as the rest of the app. Painters/Helpers, Vehicles, Business Expenses & Overhead, Taxes & Filing, and Backup & Data each collapse to a header with a glance line (roster count, vehicle count, expense totals, tax buckets), and the section you just acted on, saving vehicle owners, logging an expense, switching the year, or running a receipt search, opens automatically so the result is on screen.

= 1.0.207 =
* New: a duplicate finder next to the receipt search. One click scans the whole year for expenses sharing the same date and amount, groups them, flags same-job same-vendor pairs as "Likely duplicate", and links each entry's receipt image and job so you can verify before removing anything.
* Improve: the vehicle expense form (gas receipts) now matches the job Add Expense flow, receipt photo/PDF and the AI scan at the top, details below.

= 1.0.206 =
* Improve: the receipt search now suggests where to file an unmatched paper receipt. Under the results, "Holding a receipt that isn't listed?" lists jobs that were working around the searched dates, jobs with crew hours actually logged in the window sort first (the strongest signal someone was on site buying materials), with the scheduled work window as the backup signal, and a one-click "File Receipt Here" button on each.

= 1.0.205 =
* Improve: the receipt search's checkmark now opens the receipt file itself ("View receipt", with numbered links when an expense has several files), and searching updates the results in place without reloading the Business page. If scripts are unavailable the plain submit jumps straight back to the search section.

= 1.0.204 =
* New: Receipt search on the Business page, under the job receipt coversheets. Pick a date range and see every logged job expense in it, with vendor, category, amount, the job it's tied to, and whether a digital copy is attached, built for checking a stack of paper receipts against the books.

= 1.0.203 =
* Improve: board headers switched to the accent-underline style, a short stage-colored bar under each title with the count in a matching solid pill (icons removed).

= 1.0.202 =
* Improve: the Job Board column headers got their new look, a stage icon (inbox, roller, check) with the count as a solid color pill, steel blue for Requests, green for Active Jobs, gray for Completed, matching the card stripe palette.
* Fix: the night-before job email no longer says "0 Day Job Begins Tomorrow" for visits saved without a day count, it floors to 1.

= 1.0.201 =
* Improve: every customer-contact tap now lands in the communications log, Call/Text/Email in the Not Won drawer, the job page's customer Call/Text reveal, Call Customer and Text Customer in the job actions menu, and the Text Follow-Up nudge, alongside the quote email sends and Text Quote taps already logged. Each entry is honest about what it records: phone and mail links open the app, so the tap is what gets logged. Team-member Call/Text stays unlogged on purpose, that's coworker contact, not customer history.

= 1.0.200 =
* Improve: the board's three columns now start their cards on the same line (the glance slot under each header is fixed-height), status pills say Bid or Estimate when that's what the record is ("Bid Created" instead of "Quote Created"), and request cards show a green "Emailed Jul 20th / Texted Jul 21st" line from the communications log. Tapping a Text Quote/Bid link now logs the tap (delivery itself can't be tracked from an sms: link), and emails were already logged on send. The Not Won drawer gained Call / Text / Email links plus a History drawer on every lost lead, shown even when empty.

= 1.0.199 =
* Improve: Job Board polish pass. Requests sort by urgency (overdue visits first, then stale quotes, then newest). Each column header shows a glance line like "1 overdue visit" or "2 unpaid, $3,150 outstanding" (dollar figures only for financial viewers). Every column now collapses its long tail behind a show-more drawer with the fetch limit raised to 50. On phones, Active Jobs stacks first, columns collapse on tap, and Completed starts collapsed. Empty columns teach the flow instead of saying "Nothing here."

= 1.0.198 =
* Improve: every Job Board card (Requests, Active Jobs, Completed) now leads with one stripe that says it all, like "Upcoming - Starts Tomorrow Jul 24th", "Scheduled 4 Days Ago Jul 19th" (amber once a visit date slips), or "Completed Jul 20th", and the separate date line is gone. The whole upper card is now one big link to the job view, replacing the name link and the View Job footer link.
* Fix: records marked Not Won no longer appear on the Calendar (they still live in the board's Not Won drawer). Punch-list and callback visits now copy the parent job's State field, and the customer page merges address variants that only differed by a missing state, so no more duplicate "Other addresses on file".

Older entries: the full version history ships with the plugin's development notes and on paintoperations.com.
