# Property Portal FAQ ## General What is Property Portal? It's system to help manage properties. It will allow property managers or property owners to track expenses, rental status, tenants and tickets related to property issues. Who is Property Portal for? It's for people who manage properties like property agents, property managers and property owners. Are there limits on the lookup lists my company can create? Yes. A company can have up to 32 active Structure Types (64 total including inactive) and up to 64 active Vendor Types (128 total including inactive). Once a limit is reached, attempting to add another one returns an error. Soft-deactivated (inactive) records count toward the total limit but not the active limit. How much data does my company use? Each company's total footprint can be queried via `GET /api/companies/:id/footprint`. It combines the size of all the company's records in the database with the size of its uploaded files (receipts, property photos, documents, the company logo). Uploaded files are counted directly from their on-disk size, so it always reflects what is actually stored. The same response also reports the company's metered AI assistant usage (tokens and cost at the deepseek billing rates) and whether it is over its monthly AI budget. Only a Company Admin for that company can view it. What languages is the interface available in? The interface is available in English, German, Albanian, Croatian, Turkish, Georgian, Czech, and Serbian. A language menu in the top bar switches the whole app instantly (labels, help topics, and public pages), and your choice is remembered for next time. --- ## User Roles What user roles are there? There are two roles you will encounter day to day. Company Admin has full access within their own company and can manage users. User handles day-to-day operations for the properties they are assigned to. How do I add or change a user's role? When adding a user, select their role (Company Admin or User) in the form. To change an existing user's role, open that user's edit page and pick the new role. A Company Admin can only set these two roles and cannot change their own role. What can a User see versus a Company Admin? For properties, Company Admins see all properties in their company while Users only see the ones explicitly assigned to them. For tickets, expenses, payments, leases, and recurring expenses, Users only see records for properties in their managed list while Company Admins see everything in their company. The same rule applies to prospects (the CRM) and tenants: Company Admins see all prospects and all tenants in the company, while Users only see prospects and tenants linked to properties they manage. --- ## CRM / Prospects What is the CRM / Prospects feature? The Prospects page tracks potential renters or buyers through a pipeline: New → Contacted → Viewing → Offer → Closed Won / Closed Lost. Each prospect stores contact details, an optional linked property, a next follow-up date, an estimated price, commission terms, an interaction log, and tasks. It's how you track leads before they become tenants. How do I convert a prospect to a tenant? Open the Prospects page and click the "Convert to tenant" icon on an open prospect. This marks the prospect Closed Won and automatically creates a tenant from its contact details (same company). The prospect keeps a link to the created tenant. What is the interaction log? In the prospect page (web: click the pencil on the Prospects list; mobile: the prospect detail screen), the Interaction Log records every past call, email, or meeting against the prospect. Each entry is tagged as a Call, Email, Meeting, or Note with its text and date, so you can see what was said and when. On the web, the log is shown as a table; you can add entries and remove the ones you entered. The prospect page is for managing the prospect (stage, follow-ups, notes, tasks); personal details are edited separately via the "Edit Details" button. What are prospect tasks? Each prospect can have multiple tasks (e.g. "Send offer email", "Call back") with an optional due date and a done flag. On mobile, tap the circle next to a task to mark it done. Open tasks also make a prospect appear in the "Upcoming Follow-ups" dashboard list. What are the CRM reports? The Reports page has a CRM tab showing prospects by stage, open/won/lost totals, the conversion rate (won ÷ closed), a weighted commission forecast, and an "Upcoming Follow-ups" list. The forecast estimates expected commission across your open pipeline using fixed stage probabilities (New 10%, Contacted 25%, Viewing 50%, Offer 75%). The follow-ups list surfaces open prospects that need attention — a next follow-up within 7 days or any open task — sorted by soonest date. Who can see which prospects? Company Admins (role 3) see all prospects in their company. Company Users (role 4) only see prospects linked to properties they manage — and can only convert those. Can I ask the AI assistant about my prospects? Yes. The AI assistant understands prospects like it does tenants and properties. You can ask questions such as "How many prospects are at stage X?", "How many prospects do we have for property X?", "Who should I follow up with next?", or "What stage is John Doe's prospect at?". The assistant counts prospects per stage, lists prospects for a named property, finds the next follow-up dates, and looks up prospect contact details. Can the AI assistant create records for me? Yes. The assistant can create prospects, vendors, tickets, expenses, properties, and tenants directly from chat. It will always ask you to confirm before creating anything, and nothing is written until you confirm. For example, you can say "Create a ticket for property X: leaking tap, due Friday" or "Add a vendor called Green Energy for the office building". Creating a property from chat only needs the name and address — you finish the remaining details in the app. The same permission rules apply as the app itself: only a Company Admin (or a user who manages the property) can create, and the free plan's 5-property limit still applies. Can I share my listings publicly with a public page? Yes. A Company Admin can enable a public page for the company from the company settings: pick a public URL name (e.g. "bavaria-immobilien"), and your page is live at `.../public/` with no login required. The page showcases only your properties with a "For Rent" or "For Sale" status. Each property has a "Publicly listed" toggle that is on by default (it reads "Not listed publicly" when off) — turn it off on a property to hide that listing from your public page. The public page deliberately shows only safe listing info (photos, name, city, price, size, beds/baths) — never your street address, GPS coordinates, national IDs, or internal notes. Visitors can search your listings and narrow them by "For rent"/"For sale", and submit a contact form on the page; each submission becomes a prospect in your CRM (stage "new", source "public-website") so you can follow up. Country pages (`/public/countries/`) aggregate listings from all opted-in companies in a country and have a country switcher (flags + listing counts) plus a language menu (English, Deutsch, Shqip, Hrvatski, Türkçe, ქართული, Čeština, Srpski). The landing page links to a countries index (`/public/countries`) that lists all countries with public listings. You can disable the public page at any time. Property status badges (e.g. "For Rent") on the public pages are shown in the visitor's language via the language menu. --- ## Login and Passwords Why won't my password work when I try to log in? Passwords are hashed before being sent, so plain text will not work. The web UI handles this automatically. Just type your normal password into the login form and it will work. Can I change my own password? Can an admin reset mine? Yes to both. Any logged-in user can change their own password from their account settings. Company Admins can reset another user's password without needing the old one. Why do I need to confirm my email after signing up? Signing up creates your account in an unconfirmed state. A 6-digit code and a magic link are sent to your email; confirm either one to unlock your account. This keeps accounts from being created with someone else's address. Use the resend link if the email doesn't arrive. What is the free plan and how long is the trial? Every new company starts on the free plan with a 30-day trial. During the trial you can use the full system, including up to 5 properties. When the trial ends you can keep using the web app free forever within the free limits — 5 properties and 500 MB of storage. The mobile app and the AI assistant are paid features: they unlock when you upgrade to the Pro plan. What happens when I reach the free plan limit? The free plan allows 5 properties. When you try to add a 6th, the web app shows an upgrade prompt instead of saving it. Your existing properties are never deleted — you just can't add more until you upgrade. A banner on the properties page shows your plan and how many free properties you've used. What currencies can I be billed in? Pro pricing is available in EUR, GBP, and USD — €15, £13, or $16 per month. Your billing currency follows your company's market (UK → GBP, US → USD, EU → EUR) and you can change it from the Plans & pricing dialog, opened via the Upgrade button on the plan banner. Payments launch shortly, so the Pro card currently shows a "Coming soon" button; once the payment processor lands, you'll upgrade from the same dialog. --- ## File Uploads How do file uploads work? When you attach a file like a receipt or lease document through the web UI, the file is sent to the server, saved to disk, and the path is stored in the database. When you view the record later, the file is loaded and displayed again. Each uploaded file must be under 10 MB. Images (like photo receipts) are automatically resized down to at most 1280px and stored as JPEG — or kept as PNG when they have transparency — and other files are stored as-is or compressed to save space. All of this is transparent; you always receive back the original-looking file. Property photos are uploaded through the property edit page. The server resizes them automatically. What is the difference between soft-delete and hard-delete? Soft delete marks the record as inactive but keeps it in the database. This is used for Tenants, Leases, Vendors, and Companies. Hard delete permanently removes the record. This is used for Properties, Expenses, Invoices, and Payments. --- ## Properties What is a Property in Property Portal? A Property is the core entity in the system — a building, apartment, house, condo, or any real estate unit you manage. Each property stores detailed information including its name, address (street, city, state, postal code, country, GPS coordinates, and national ID), physical details (square meters, bedrooms, bathrooms, total rooms, floor number, elevator count, parking spaces), and financial details for rental, purchase, or sale scenarios. You can also attach photos and documents to each property. How do I create a new property? Navigate to the Properties page and click the "Add Property" button. You will be taken to a form where you can enter the property name (required), address details, physical specifications, and optionally fill in rental pricing, purchase information, or sale asking prices. You can also upload photos (which get automatically compressed and resized to about 500KB) and attach documents like title deeds or inspection reports. Once you save, the property is created and automatically assigned to your managed list. The property's status (e.g., "For Rent," "Vacant," "For Sale") and structure type (e.g., "Apartment," "House," "Condo") are selected from lookup lists managed by your company admin. How do I view my properties? The All Properties page shows a card-based layout with each property displaying its cover photo, name, status (shown as a colored chip), key stats (bedrooms, bathrooms, square meters), and formatted price. Click on any property card to see its full detail page, which includes a photo carousel, the complete address, description, and tabs showing recent expenses and recent tickets linked to that property. Documents attached to the property are shown as downloadable icons. How do I edit a property? From the property detail page, click the "Edit" button. This opens the property form pre-filled with all existing data. You can update any field — change the status, adjust rental pricing, upload new photos or documents, remove existing ones, or update address details. Changes are saved immediately when you submit the form. Company Admins (role 3) can edit every property in their company; Users (role 4) can edit the properties assigned to them (their managed list). How do I delete a property? Only Company Admins and above can delete a property. Navigate to the property detail page and use the delete option. Important: Property deletion is a hard delete — the record is permanently removed from the database along with all its associated photos and document files from the server. There is no undo. Consider deactivating the property by changing its status instead if you may need the data later. What filters can I use on the Properties page? You can filter properties by Status (multi-select from available statuses like "For Rent" or "Vacant"), City (text search), Bedrooms (select from 1 to 4+), Minimum Square Meters, Parking (Yes/No), Elevator (Yes/No), and Structure Type (e.g., Apartment, House). All filters combine together — for example, you can find all 2-bedroom apartments with parking in a specific city. How do property photos work? When you add photos to a property, the server automatically compresses them to a target size of around 500KB by reducing quality and scaling dimensions. The first photo you add becomes the cover photo (displayed first on the property card and detail page). You can change which photo is the cover. Photos are stored as JPEG files on the server, and only the file path is saved in the database. When you view the property, the photos are loaded and displayed in the browser. What property documents can I upload? You can attach any document files to a property — PDFs, Word documents, images, or other file types. These appear in a documents section on the property detail page. Click on any document to download it. Documents are stored on the server filesystem, not in the database. Do I have to enter a country? Yes. The **Country** field is required when creating a property (and when updating its address). The form will not save until a country is entered, and the API rejects requests with a missing or blank country. The country also feeds the public Marketplace country pages — properties without a recognized country simply don't appear on `/public/countries/` pages. --- ## Marketplace What is the Marketplace? The Marketplace is the public side of the portal — the pages at `/public/countries/` (a country index) and `/public/` (a company's public showcase). It lets anyone browse for-rent and for-sale properties and submit a lead without logging in. How do properties get listed in the Marketplace? A property appears publicly only when its "Publicly listed" toggle is on and its status is **For Rent** or **For Sale**. Properties with a status of **Sold** or **Rented** are automatically de-listed from the Marketplace — the "Publicly listed" toggle is forced off and cannot be re-enabled until the status changes. What is my company's public name? Your company's public name is the URL slug for your Marketplace page (for example, `bavaria-immobilien` in `/public/bavaria-immobilien`). A company admin sets it once from the Company Admin section under Marketplace. Once set, it is permanent and cannot be changed or disabled, so choose carefully. What is a property detail page? Clicking any property card on a Marketplace company page or country page opens a dedicated detail page at `/public/property/`. It shows a photo carousel at the top, the price, an overview grid (property type, bedrooms, bathrooms, size, floor, parking, elevator), the description, and breadcrumbs (Home → Browse properties by country → Country → Agency → Property). A clear **"I'm interested"** button opens the same contact form as the agency page — each submission creates a prospect in the company's CRM. The detail page follows the same privacy rules as the rest of the Marketplace: no street address, GPS coordinates, national IDs, or documents. What happens when I visit a Marketplace page that doesn't exist? A removed property (`/public/property/`), an unknown country code (`/public/countries/`), or a missing agency (`/public/`) each show a clear, translated "not found" message instead of an empty-looking page or the search's "no matches" text. What are Lead Sources? Lead Sources is a company-level lookup list (like Structure Types and Vendor Types) that tracks where your prospects come from — for example Website, Referral, Marketplace, or Social Media. Company admins manage the list from the Control Panel under Database Items → Lead Sources. When creating or editing a prospect you can tag it with a lead source. --- ## Tickets What is a Ticket? A Ticket records an issue or maintenance request for a property. Think of it as a support ticket for a physical location — something like "Leaky faucet in unit 3" or "Broken elevator in the lobby." Each ticket is linked to a specific property and tracks what the problem is, how severe it is, what its current status is, and when it needs to be resolved by. How do I create a ticket? Go to the Tickets page and click "Add Ticket." You will need to select the property the issue relates to (from a list of your properties), choose a status (e.g., Open, In Progress), set the severity (Critical, High, Medium, or Low), enter a description of the issue, and pick a due date for when it should be resolved. You can optionally set a resolved date if the issue is already fixed. Once created, the ticket appears in the list and on the property's detail page. How do I view and manage tickets? The Tickets page shows a data table with columns for Property, Status, Due Date, and Severity. The status is color-coded — closed/resolved tickets show green, in-progress ones show blue, open ones show orange. If a due date has passed, it turns red and bold so overdue items stand out. You can expand any row to see the full description and who created the ticket. At the top of the page, summary cards show your total tickets, how many are open, how many are high/critical severity, and how many are due within the next 7 days. How do I update a ticket? Click the pencil icon on any row in the tickets table. An inline edit dialog opens where you can change the description, status, severity, and due date. The system logs who made the change and when. If you resolve an issue, change the status to "Closed" and optionally set the resolved date. How do I delete a ticket? Only Company Admins and above can delete tickets. Ticket deletion is a hard delete — the record is permanently removed. Regular users do not see a delete option. If a ticket is no longer relevant, consider changing its status to "Closed" instead of deleting it. What filters are available on the Tickets page? You can filter by property, status (by exact status or by slug like "open"/"in_progress"), severity, who created the ticket, and date ranges for when the ticket was created, when it's due, or when it was resolved. You can also toggle between showing tickets from the last 30 days (Recent) or all historical tickets. A text search box lets you search across all fields on the client side. What are the default ticket statuses? The system comes with three default ticket statuses: Open, In Progress, and Closed. Your company admin can create additional statuses or rename existing ones through the company settings. The status determines how tickets are counted on the dashboard summary cards. Can I attach files to a ticket? No. Tickets are text-based issue trackers and do not support file or receipt attachments. If you need to attach documentation to a cost, use the Expenses feature instead. --- ## Expenses **The three types of expenses — which one is which?** | Type | What it is | When to use it | |---|---|---| | **Expense** | Every cost record — one-off or recurring. One page, filterable by *One-off* / *Recurring*. | Any cost associated with a property. | | **Automatic Expense** | A set-once template (property, amount, frequency) that **spawns an expense record every period**. Two amount modes: **fixed** (constant) and **variable** (changes each period — e.g. electricity). | Recurring costs you want the system to generate for you. | **Which should I use?** One-off → **Expense**. Repeating, manual → **Expense** with the *Recurring* toggle. Repeating and you want it automated → **Automatic Expense** ("set it and forget it" — generated entries appear on the Expenses page under the *Recurring* tab with an "auto" badge, where you mark them Paid and attach receipts). All are scoped by role: Company Admins see everything in their company; Users only see records for properties they manage. What is an Expense? An Expense records a cost associated with a property. This could be a one-off repair bill, a service fee, or a recurring cost (water, electricity, insurance). Each expense is linked to a property, categorized by expense type, can carry a vendor and a period (for recurring entries), and can have receipt files attached as proof of payment. How do I create an expense? Navigate to the Expenses page and click "Add Expense." Select the property, choose a category, enter the amount, pick the currency, set the date, and select a status (typically "Pending" if not yet paid or "Paid" if settled). To record a recurring cost by hand, switch on the "Recurring expense" toggle and optionally add a vendor and a period. You can add notes, a reference number, and attach receipt files — PDFs, Word documents, or images. The system defaults the currency to EUR if you don't specify one. How do receipt attachments work for expenses? When you add a receipt, the file is encoded and sent to the server, which saves it to the filesystem under `./files/receipts/`. Only the file path is stored in the database. When you view the expense later, the receipt is loaded from disk and displayed in the browser. You can download any attached receipt by clicking on it. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Can I edit or delete an expense that has already been paid? No. Once an expense's status is changed to "Paid," it becomes locked. Neither the update nor the delete option is available — both in the UI and enforced by the API. This protects your financial records from accidental changes. If you need to modify a paid expense, you must be an admin and handle it through data management processes. How do I edit an expense? Click the pencil icon on any row in the Expenses table. An inline dialog opens allowing you to change the status and manage receipts. Only the user who created the expense or an Company Admin can edit it. How do I delete an expense? Only users with admin-level roles (Company Admin and above) can delete an expense. Deletion is a hard delete — the record is permanently removed from the database, and any attached receipt files are also deleted from the server. You cannot delete an expense whose status is "Paid." What are the tabs on the Expenses page? Three tabs: **All**, **One-off**, and **Recurring**. Auto-generated entries (from Automatic Expenses) carry a small "auto" badge, and their expand row shows the template that produced them. What filters are available on the Expenses page? You can filter by property, category, currency, status (by slug like "pending" or "paid"), amount range (minimum and maximum), and expense date range. If you are a User and don't specify a property, the list automatically shows only expenses for properties you manage. You can also toggle between Recent (last 30 days) and Historical (all time) views, and use the search box to find specific entries. What do the summary cards at the top of the Expenses page show? Four summary cards give you a quick financial overview: Total Expenses (the sum converted to EUR), This Month's Total (expenses from the current month), the Average expense amount, and the Total number of expense entries. There is also a breakdown by currency showing the original amounts. Can I export expenses to CSV? Yes. Click the "CSV" button on the Expenses page to download all currently displayed expenses as a CSV file. The export respects any active filters you have applied. --- ## Automatic Expenses **Note:** an Automatic Expense is the template that generates recurring expense records each period. See the Expenses section above for the comparison, and the in-app help topic "Automatic Expenses" for details. What is an Automatic Expense? An Automatic Expense is a set-once recurring cost attached to a property — mortgage, insurance premium, ground rent, service charge, or electricity. You define the template once (amount, amount mode, frequency, vendor, due day), and the system automatically generates an Expense record each period, so nobody has to re-enter it. What are the two amount modes? **Fixed** — the amount is constant every period (mortgage, insurance premium); the template amount is written into each generated entry exactly. **Variable** — the amount changes each period (electricity, water); the template stores the last-known amount as a pre-fill, the generated entry arrives Pending with that amount, you adjust it to the actual bill, and the system remembers the final Paid amount as the next pre-fill. How is an Automatic Expense different from a recurring Expense? A recurring Expense is one record you enter by hand (or that a template generates) for a specific period. An Automatic Expense is the template that generates those records every period (monthly, quarterly, or yearly). Generated records appear on the Expenses page under the Recurring tab with an "auto" badge and link back to their template. How do I create an Automatic Expense? Go to the Automatic Expenses page and click "Add Automatic Expense." Choose the property, category, vendor, amount, amount mode, currency, frequency, start date (defaults to today), optional end date, due day, and whether it is charged through to the tenant. The system starts generating entries from the start date. Tip: leave the start date at the current month to avoid backfilling past periods. How does the automatic generation work? Every night a background job checks your active templates and creates an Expense record for every period that hasn't been generated yet — Pending status, due date from the template, and (for variable templates) the last-known amount as a pre-fill. You treat each generated entry like any expense: adjust the amount, mark it Paid, attach the receipt. What happens with the "Paid by Tenant" option? Each generated entry also creates a Pending invoice for the active tenant on that property (same behaviour as a manual paid-by-tenant expense). If there is no active lease or the vendor is missing, the entry is still created but the invoice is skipped (and logged). Can I edit or delete a paid expense? No. Once an expense's status is changed to "Paid," it becomes locked — the same rule applies to one-off and recurring expenses. You cannot update or delete it. This protects your financial audit trail. How do edits to an Automatic Expense apply? Changes apply from the next period onward — already-generated entries keep their amounts, so your financial history stays accurate. You can pause a template (stops generation) or set an end date. How do I delete an Automatic Expense? Only admin-level users (Company Admin and above) can delete an automatic expense. Deleting a template stops future generation but keeps all already-generated records as history. Who can see which automatic expenses? Access is scoped by role: Company Admins see all automatic expenses in their company; Users (property managers) only see automatic expenses for properties they manage. Site administrators see all companies. --- ## Vendors What is a Vendor? A Vendor is a company or individual that provides services or supplies to your properties. This includes plumbers, electricians, cleaning services, utility companies, landscapers, and any other service provider you work with. Each vendor record stores contact information, account details, and what type of vendor they are. How do I create a vendor? Go to the Vendors page and click "Add Vendor." You will need to provide the vendor's name (required) and select their vendor type (e.g., "Plumber," "Electrician," "Utility Company") from the vendor types list. You can optionally link the vendor to a specific property, add an account number, phone numbers (up to two), email address, website, and a contact person's name. A description field lets you add any notes about the vendor. How do I view and manage vendors? The Vendors page shows a data table with columns for Name, Type, Property, Account, and Active status. Expand any row to see full contact details including phone numbers, email, website, contact person, and description. Summary cards at the top show Total vendors, Active count, Inactive count, and how many have account numbers on file. How do I edit a vendor? Click the pencil icon on any vendor row. An inline dialog opens where you can modify the description, vendor type, account number, phone numbers, email, website, contact person, and toggle the active status. All changes are saved immediately. How do I delete a vendor? Vendors use soft-delete — clicking delete deactivates the vendor by setting them as inactive rather than removing them from the database. The vendor record stays in the system but is hidden from the default view. Only Company Admins and above can deactivate a vendor. To permanently remove a vendor, an admin would need database access. Deactivated vendors can be reactivated by editing them and toggling the active switch back on. What filters are available on the Vendors page? You can filter by name, vendor type, property, account number, phone numbers, email, website, and contact person. All filters are case-insensitive text searches. A search box provides client-side filtering across name, account, phones, email, and contact fields. How do vendors relate to expenses? Vendors are primarily linked to recurring expenses. When you create a recurring expense, you select which vendor provided the service. If that expense is marked as "Paid by Tenant," the vendor's name and type are included in the auto-generated invoice notes so the tenant knows who the charge is from. Regular (one-time) expenses do not link to vendors. --- ## Leases What is a Lease? A Lease is a rental agreement between you (the property manager/owner) and a tenant for a specific property. It captures the terms of the rental — who the tenant is, which property they are renting, the start and end dates of the agreement, the rent amount and payment period, the security deposit, and whether rent invoices should be auto-generated. Lease documents like signed contracts can be attached as files. How do I create a lease? Go to the Leases page and click "Add Lease." The creation form has several sections: - **Property & Tenant**: Select the property being leased and the tenant from your tenant list. Both are required. - **Lease Term**: Set the start date and end date of the lease agreement. - **Financial Details**: Enter the periodic rent amount, select the currency (defaults to EUR), choose the billing period in days (options typically include 14, 30, 45, 60, or 90 days), enter the security deposit amount, and toggle Auto-Billing on if you want rent invoices generated automatically. - **Documents**: Upload the signed lease agreement or any related documents (PDF, DOC, or image files). How do I view and manage leases? The Leases page displays a data table with columns for Property Name, Tenant Name, Rent amount, Auto-Billing status (shown as a check icon if enabled), number of documents, and action buttons. Expand any row to see the deposit, billing period, start date, and end date. Summary cards at the top show the total number of leases, the average rent, and the total deposit amount across all leases. How do I edit a lease? Unlike other modules, leases do not have a dedicated edit page. Click the pencil icon on any lease row to open an inline dialog where you can manage the lease documents — add new files or remove existing ones. The financial terms (rent, deposit, dates, period) are not editable through this dialog. If you need to change lease terms, you would typically end the current lease and create a new one. How do I delete/deactivate a lease? Leases use soft-delete. Click the "Deactivate" button in the expanded row of any lease. A confirmation dialog asks if you are sure — deactivating a lease removes it from the active leases list but keeps the record in the database. Only Company Admins and above can deactivate a lease. Deactivated leases can be reactivated if needed. What is Auto-Billing and how does it work? Auto-Billing is a feature that automatically generates rent invoices for a lease on a set schedule. When you enable Auto-Billing on a lease and set a billing period (e.g., 30 days for monthly rent), a nightly service checks if any new billing periods have completed and creates Pending invoices accordingly. For 30-day periods, it uses calendar months; for other periods (14, 45, 60, 90 days), it divides the elapsed time by the period length. The system is smart about duplicates — it checks if an invoice already exists for that lease and period before creating a new one, so running it multiple times is safe. How do lease documents work? When creating or editing a lease, you can upload documents like the signed rental contract. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Documents are stored on the server filesystem, and the file path is saved in the database. On the leases list page, a chip shows the document count (e.g., "2 docs") — click it to see a menu of files available for download. What filters are available on the Leases page? You can filter by Property (select from your properties), Tenant (select from your tenants), Status (Active/Inactive/All), Auto-Billing (Any/Yes/No), and End Date range (from/to). A text search box lets you search across property name, tenant name, rent, deposit, and period. Can I download invoices from the Leases page? Yes. Click the red PDF icon on any lease row to generate and download a PDF invoice with Property Sage branding and all the invoice details. Can I export leases to CSV? Yes. Click the "CSV" button to download all currently displayed leases (respecting your active filters) as a CSV file. What happens when a lease ends? When the end date of a lease is reached, the auto-billing service stops generating new invoices for that lease. The lease remains in the system so you can reference it later. On your dashboard, the "Leases Ending Soon" widget shows leases that are within 30 days of their end date, helping you plan renewals or find new tenants. --- ## Tenants What is a Tenant? A Tenant is a person who rents a property from you. The system stores their basic contact information (first and last name, email, phone number), keeps track of whether they are currently active, and lets you attach documents related to them (like identification documents or guarantor forms). Tenants are linked to properties through leases. How do I create a tenant? Go to the Tenants page and click "Add Tenant." Fill in the tenant's first name (required) and last name (required), plus their email and phone number (optional). You can also upload documents like a copy of their ID, proof of income, or guarantor forms. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Once saved, the tenant becomes available to link to leases. How do I view and manage tenants? The Tenants page shows a data table with columns for Name, Email, Phone, Active status (shown as a green or red chip), and Document count. Summary cards at the top display Total tenants, Active count, and Inactive count. Click the document count chip on any row to see a menu of uploaded files that you can download. How do I edit a tenant? Click the pencil icon on any tenant row. An inline dialog opens where you can update the email, phone number, toggle the active status, and manage documents — upload new files or remove existing ones. The tenant's first and last names are preserved from the original creation and are sent back unchanged with the update. How do I delete a tenant? Tenants use soft-delete — clicking delete deactivates the tenant by setting them as inactive. The record stays in the database with all its data, so you can reference historical leases and documents. Deactivated tenants are hidden from the default view but can be revealed using the active filter. To permanently remove a tenant, database access would be needed. Deactivated tenants can be reactivated by an admin editing the record and toggling active back on. What filters are available on the Tenants page? You can filter by name (searches both first and last name), email, and phone — all case-insensitive text searches. By default, only active tenants are shown. The API supports filtering by active status (true, false, or all), but the current UI defaults to showing only active tenants. How do tenant documents work? When you add documents to a tenant record, they are saved to the server filesystem under `./files/tenant-documents/` and only the file path is stored in the database. On the tenants list page, the document count is shown as a clickable chip — click it to see a list of document names and download individual files. Documents can be uploaded when creating a tenant or added later through the edit dialog. How do tenants relate to leases? Tenants are linked to properties through leases. When you create a lease, you select a tenant and a property, forming the rental agreement. A tenant can have multiple leases (e.g., if they rent multiple properties or have renewed their lease over time). The tenant record itself does not store lease information — you find a tenant's leases by looking at the Leases page filtered by that tenant. --- ## Payments What are Payments? Payments record when a tenant has paid an invoice. This to help track and record that an invoice payment has been settled. What happens when I create a payment? The linked invoice status automatically changes to Paid. What if I delete a payment? If you delete a payment, which Company Admins can do, the invoice status goes back to Pending. What happens when I mark a recurring expense as paid by tenant? The system automatically creates a Pending invoice for that tenant. It looks up the active lease on that property, grabs the vendor details, and sets the due date 15 days out. Can I edit or delete an expense that has already been paid? No. Once an expense is marked Paid, it is locked. You cannot update or delete it. --- ## Invoices What are Invoices? Invoices are automatically generated and billed to a tenant. This is for rent or for a recurring expense. When are Invoices generated? There are two occasions, one is every night the system will automatically detect if rent is due based on the lease term and period. For more details see (link) leases. The second occasion is when a recurring expense is to be paid by the tenant. These are for things like water or electricity. Once the expense has been set to "Paid by tenant" the system will automatically generate an invoice to the tenant. Can I download a PDF of an invoice? Yes. On both the Invoices and Leases pages, click the red PDF icon on any row. The system generates a PDF on the fly with Property Sage branding and all the invoice details, then downloads it automatically. --- ## Chat Assistant How does the chat assistant work? The chat runs on a locally hosted AI server. It has access to your property data through a set of tools and always looks up information before answering, so it never makes up numbers. You can ask it questions about properties, tickets, leases, invoices, payments, expenses, vendors, and tenants. If it cannot find an answer, it says I cannot answer that. What kind of questions can I ask? For properties you can ask how many are for rent, show vacant apartments, or which have 3 bedrooms with parking. For tickets you can ask how many are open or show critical ones. For leases you can ask how many are active or which end this month. For invoices you can ask how many are overdue or list overdue ones with tenant names. For payments you can ask what was collected or which methods were used. For expenses you can ask totals by category or which property had the most. For vendors you can ask how many are active or list by type. For tenants you can ask for counts, names, or contact info. Can the chat answer how-to questions too? Yes. The assistant can also answer questions about how the system works, like what is an invoice, how do file uploads work, or what are the user roles. Does the chat understand lowercase names? Yes. If you type james smith instead of James Smith, it will still find the right person. Does each user have their own chat history? Yes. Each user has one persistent conversation. The full history loads every time you open the chat page. Is there a limit on how much I can use the chat? Each user can send up to 100 chat requests per day. The counter resets automatically at midnight. If you reach the limit, the chat will tell you to try again tomorrow. Companies also share a monthly AI budget (default $3.50, configurable via `AI_MONTHLY_BUDGET_USD`) covering paid deepseek answers. Once spent, the chat says the monthly usage limit has been reached and waits for the 1st of the next month; answers served from the cached FAQ or an exact cached reply still work. --- ## Bug Reports How do I report a problem with the site? Click the bug icon in the top bar (or on mobile, open Settings → Report a Problem). Fill in a short title and description, choose a category (Bug, Feature request, Other) and an urgency, then submit. The page you were on is recorded automatically so we know where it happened. Who can see my bug reports? You can only see the bug reports you submitted. Company Admins can see all reports from their company. Can I follow up on my report? Your submissions appear in the "My Reports" list, with their status (open, in-progress, resolved, closed). Only admins change the status. Can I discuss a bug with the developers? Yes. Open any bug report from the Bugs list — a dialog shows the full conversation and a reply box. You can add detail and the developers can ask questions. Replies are allowed until the bug is marked closed. The list shows how many replies each bug has so you can see at a glance when something new arrives. --- ## Auto-Billing How does auto-billing work? A service runs nightly to generate rent invoices. It finds all active leases with auto-billing enabled, calculates which billing periods have completed, and creates a Pending invoice for each one. Is it safe to run auto-billing multiple times? Yes. It checks for existing invoices before creating new ones, so duplicates are never created. --- ## Search and Filters What filters can I use on each page? On the Leases page you can filter by Property, Tenant, Status, Auto-Billing, and End Date range. On Payments you can filter by Payment Method, Min or Max Amount, and Date range. On Properties you can filter by Type, City, Bedrooms, Size, Parking, Elevator, and Structure. On Tenants you can filter by Name, Email, Phone, and Active status. On Vendors you can filter by Name, Vendor Type, Property, and Account. Can I combine multiple filters? Yes. All active filters combine together. For example, on the Leases page you can filter by a specific property with auto-billing enabled and an end date within a certain range all at once. --- ## CSV Export Which pages let me download a CSV? Six pages have a CSV button: Tickets, Expenses, Payments, Invoices, and Leases. The CSV includes the record ID, all visible fields, and a currency column. Attachments and photos are excluded. Does the CSV respect my current filters? Yes. Whatever data is currently displayed, including active search filters, is what gets exported. --- ## Compliance What is compliance tracking? Compliance tracking lets you record the regulatory certificates and registrations each property needs — energy performance certificates, title-deed registrations, rental-registry entries, and more. Each certificate stores its type, reference number, issue and expiry dates, and an optional uploaded document. The system flags certificates that are expiring soon or already expired. It is a per-property opt-in: enable compliance tracking in a property's settings to track certificates, renewals, and market requirements for it. Where can I see compliance data? On each property page there is a Compliance section listing the certificates with their status (valid, expiring, or expired), shown once compliance tracking is enabled for the property. The dashboard shows a compliance widget that counts, across your portfolio, how many certificates need attention (expiring or expired), and how many properties are not yet tracking compliance. Is compliance tracking free? Yes. Recording and viewing certificates is part of the free plan. The market-specific compliance guidance from the AI assistant is a Pro feature. Does the required-certificate list depend on my country? Yes. The required certificate types are derived from your company's country. Supported markets are Turkey, Georgia, Czechia, Serbia, Moldova, Albania, and Kosovo. In other countries you can still record certificates, but no required list is enforced. Can the AI assistant record certificates? Yes. Ask the assistant to record a certificate and it will ask for your confirmation before saving. It can also summarize compliance status and list certificates that are expiring or expired. Is this legal advice? No. Compliance guidance is informational and does not constitute legal advice. Regulatory requirements change, so verify them with a local professional. # User Experience ## Project layout ### Rest API The directory ./api/ contains the API source code written in Go using the Gin framework. The models are in ./api/models/ the controllers are in ./api/controllers/ ### Node.js User Interface (UI) The directory ./ui/ contains the node.js source code written using the Vue.js framework making use of vuetify components. Every user that logs in has a role id (saved in the mongodb as static-id) The user roles (name, id) are listed below: Company Admin, 3 User, 4 ## Interface Behavior The main interface has a header, menu sidebar on the left called 'control-panel' and a display page next to that. The footer is at the bottom and currently has minimal content. ## Sign-up & Plans Signing up is a two-step flow: 1. **Create account** — enter email + password + first/last name. The account is created unconfirmed and a 6-digit code plus a magic link are emailed. 2. **Confirm email** — enter the code on the sign-up page (which auto-advances to a code field) or open the magic link, which confirms automatically. 3. **Onboarding wizard** — after the first sign-in a wizard runs: enter your company name (pre-filled with your first + last name), website, and logo, then add your first property (or skip). Users who sign in without a company are hard-redirected here until it's done. Plans: - **Free plan**: web core (properties, leases, tenants, expenses, tickets, etc.) usable forever. Limited to 5 properties and 500 MB of storage. Every new company gets a 30-day trial. - **Pro plan**: unlocks the mobile app and the AI assistant. Requires a paid subscription. - The properties page shows a plan banner (free plan, trial end date, used/limit property count, upgrade button). Trying to add a property past the free limit shows an upgrade message (HTTP 402). - The banner's **Upgrade** button opens the **Plans & pricing** dialog: two plan cards (Free / Pro) with prices in the company's effective billing currency (€15 / £13 / $16 per month), a currency selector (EUR / GBP / USD) that persists the company's billing currency, trial status when present, and a Pro card that is visibly non-functional ("Coming soon") until payments launch. - On mobile, when a company's free trial has ended (or they were never on trial), the app shows an "Upgrade to continue" screen instead of the app content. ## User When a user with role 4 logs in the they should see the following options in the 'control-panel' with the outlined behavior: ### Properties: - In the Dashboard they only see properties they have in their MangedProperties array - When looking at all properties with they can see all properties with the same Company (CompanyID) as them - They can add new properties that will automatically be assigned to them - The All Properties page (web) opens on the **My Properties** view by default (only properties in their ManagedProperties array, including when arriving from a Dashboard link like "Properties for Rent"). A **Show All Properties** button reveals every company property and relabels to **Show My Properties**; clicking it again returns to the managed-only view - On a property detail page the **Edit** button only appears for properties in their ManagedProperties array ### Tickets: - They can see all tickets only for the properties they have in their ManagedProperties array. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property - They can add/edit tickets only for the properties they have in their ManagedProperties array ### Expenses: - They can see all expenses only for the properties they have in their ManagedProperties array. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property, Category or a Date Range - They can add/edit expenses only for the properties they have in their ManagedProperties array ### Payments: - They can see all payments only for the properties they have in their ManagedProperties array. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Invoice - They can add/edit payments only for invoices associated with properties they have in their ManagedProperties array ### Invoices: - They can see all invoices only for the properties they have in their ManagedProperties array with the invoice status of "Pending" or "Overdue". If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property - They can see historical invoices only for the properties they have in their ManagedProperties array regardless of invoice status - Each row has a red PDF icon to download a dynamically generated A4 invoice PDF with Property Sage branding ### Expenses (recurring) - They can see all expenses (one-off and recurring) only for the properties they have in their ManagedProperties array - They can add/edit expenses only for the properties they have in their ManagedProperties array - When creating a recurring expense with **Paid by Tenant** enabled, a "Pending" invoice is automatically generated with `Notes` containing the vendor name, vendor type, and invoice number ### Automatic Expenses - They can see and create automatic-expense templates only for the properties they have in their ManagedProperties array - Automatic expenses auto-generate expense records each period via the nightly job; generated rows show an "auto" badge and link back to the template - They can pause/resume a template, which stops/restarts generation without touching existing entries ### Leases - They can see all leases only for the properties they have in their ManagedProperties array - The leases page has a filter card with Property, Tenant, Status (Active/Inactive/All), Auto-Billing (Any/Yes/No), and End Date range (from/to) - They can add/edit leases only for the properties they have in their ManagedProperties array - Each row has a red PDF icon to download the latest invoice PDF for that lease ### Tenants - They can see all tenants in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Email or Last Name - The API now supports `?name=`, `?email=`, `?phone=`, and `?active=` query filters for programmatic filtering - They can add new tenants to the system ### Prospects (CRM) - They can see prospects only for the properties they manage (prospects without a linked property are hidden from them) - They can add new prospects for properties they manage and convert them to tenants; prospects for unmanaged properties are read-blocked (403) - The Reports page CRM tab only reflects their own pipeline (managed properties) - Each row can be expanded to show estimated price, commission, created date, active, and converted status without leaving the list - They can add interaction-log entries and tasks to the prospects they manage. On both web and mobile the prospect page manages the prospect — stage, next follow-up, interaction log, tasks, convert — while personal details are edited separately via "Edit Details" (web `/prospects/:id/details`, mobile opens the edit-details form). - Clicking the pencil on a prospect row opens its prospect page (`/prospects/:id`) for managing the prospect: adjust the stage, set the next follow-up date, add interaction-log entries and tasks, or convert to a tenant. Personal details (name, email, phone, property, commission) are shown read-only there; an **Edit Details** button in the top right opens `/prospects/:id/details` to change them. Deactivate lives on the Edit Details page, below the save button, so a mis-click while managing a stage or note can't deactivate the prospect. The two workflows stay separate so editing a stage or adding a note never risks changing contact info. - The AI assistant answers prospect questions (counts by stage, prospects per property, next follow-ups, contact lookup) scoped to what they can see ### Vendors - They can see all vendors in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Name or Account - They can add new vendors to the system ### Compliance - Compliance tracking is a per-property opt-in: a property only appears in compliance views (property Compliance section, dashboard widget, AI compliance tools) once its landlord enables tracking in the property settings - They can see compliance certificates (Compliance section on the property page) and the dashboard compliance widget only for the properties they manage - They can add and delete compliance certificates only for the properties they manage - The compliance summary (dashboard widget) only reflects their managed properties - The AI assistant can summarize compliance, list expiring/expired certificates, and record certificates for their managed properties; market-specific compliance guidance from the assistant is a Pro feature ## Company Admin When a user with role 3 logs in the they should see the following options in the 'control-panel' with the outlined behavior: ### Properties: - In the Dashboard they only see properties they have in their MangedProperties array - When looking at all properties with they can see all properties with the same Company (CompanyID) as them - They can add new properties that will automatically be assigned to them - The **Country** field is mandatory when creating a property or updating its address — the form and the API both reject a missing or blank country. It feeds the Marketplace country pages. ### Tickets: - They can see all tickets in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property - They can add/edit tickets only for the properties with the same CompanyID as them ### Expenses: - They can see all expenses in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property - They can add/edit expenses only for the properties with the same CompanyID as them ### Payments: - They can see all payments in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Invoice - They can add/edit payments only for the invoices associated with properties that have the same CompanyID as them ### Invoices: - They can see all invoices in the database with the same Company (CompanyID) as them with the status of "Pending" or "Overdue". If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Invoice - They can see historical invoices only for the properties that have the same Company ID as them. - Each row has a red PDF icon to download a dynamically generated A4 invoice PDF with Property Sage branding. ### Expenses (recurring) - They can see all expenses (one-off and recurring) in the database with the same Company (CompanyID) as them - They can add/edit expenses only for properties with the same CompanyID as them - When creating a recurring expense with **Paid by Tenant** enabled, a "Pending" invoice is automatically generated with `Notes` containing the vendor name, vendor type, and invoice number ### Automatic Expenses - They can see all automatic-expense templates with the same Company (CompanyID) as them, and create/edit/pause them for any property in the company - The Automatic Expenses page shows stat cards (active templates, monthly commitment, paid/pending this month), each template's next period + next due date, and pause/resume + soft-delete actions - Templates with **Paid by Tenant** generate a Pending tenant invoice per generated entry (skipped, not fatal, if the lease/vendor is missing) - Edits apply to future periods only; existing entries are never rewritten ### Leases - They can see all leases in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Property or Tenant - The leases page has a filter card with Property, Tenant, Status, Auto-Billing, and End Date range filters - They can add/edit leases only for the properties with the same CompanyID as them - Each row has a red PDF icon to download the latest invoice PDF for that lease ### Tenants - They can see only tenants linked (via leases) to properties they have in their ManagedProperties array. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Email or Last Name - They can add new tenants to the system ### Prospects (CRM) - They can see all prospects in their company and convert any of them to a tenant - The Reports page CRM tab reflects the whole company pipeline - Each row can be expanded to show estimated price, commission, created date, active, and converted status - Clicking the edit pencil opens a dedicated prospect page (`/prospects/:id`) for managing the prospect — stage, next follow-up, interaction log, and tasks — with personal details shown read-only; an **Edit Details** button top-right opens `/prospects/:id/details` to change contact info, property, and commission separately - The Reports CRM tab has an **Upcoming Follow-ups** list: open prospects with a follow-up within 7 days or any open task, sorted by soonest date - The AI assistant answers prospect questions across the whole company pipeline ### Vendors - They can see all vendors in the database with the same Company (CompanyID) as them. If that list exceeds 50, the results are paged and a search is added at the top of the displayed table that allows them to search on Name or Account - They can add new vendors to the system ### Compliance - Compliance tracking is a per-property opt-in: a property only appears in compliance views (property Compliance section, dashboard widget, AI compliance tools) once tracking is enabled in the property settings - They can see and manage compliance certificates (Compliance section on the property page) for all properties in the company - The dashboard compliance widget summarizes certificates across all company properties, including which required certificates are missing per property - The AI assistant can summarize compliance, list expiring/expired certificates, and record certificates for any company property; market-specific compliance guidance from the assistant is a Pro feature ### Company Admin The users with role 3 also has a 'Company Admin' section in the 'control-panel'. Under that they have - They can manage the company's lookup lists (structure types, vendor types, lead sources, statuses, etc.). Limits apply: a company can have up to 32 active Structure Types (64 total including inactive), up to 64 active Vendor Types (128 total including inactive), and up to 64 active Lead Sources (128 total including inactive); attempting to exceed a limit returns an error. - They can query their company's data footprint (`GET /api/companies/:id/footprint`), which reports the total size of the company's records plus its uploaded files (receipts, photos, documents, logo) along with the company's metered AI assistant usage and whether it's over its monthly AI budget. ### Marketplace The Company Admin section also contains a **Marketplace** panel where the company's public name is managed: - The company's **public name** is the URL slug for its Marketplace page (e.g. `/public/bavaria-immobilien`). - A company admin can view their current public name; if it has not been set yet, they can choose one (with availability checking). Once set, the name is **permanent** and cannot be changed or disabled — even by site admins. - Properties with a status of **Sold** or **Rented** are automatically de-listed from the Marketplace: their "Publicly listed" toggle is forced to off and cannot be re-enabled until the status changes. All file uploads (receipts, documents, photos, logos) are capped at **10 MB per file**. Uploaded images are automatically resized down to at most 1280px on the longest side and stored as JPEG (or kept as PNG when they have transparency), and other file types are stored compressed where possible — transparent to the user, who always receives the original-looking file back when viewing or downloading. #### Users: - They can see all users with the same CompanyID as them - They can edit/update all users with the same CompanyID as them - They can add new users to that Company - When listing users, each row shows the user's role and Notes appear in an expandable row - When adding a user they select the role (Company Admin or User); defaults to User - They can change another user's role (Company Admin or User only), but never their own role --- ## Payments Page Search Both User and Company Admin roles can filter the Payments page by: - **Payment method** dropdown (Bank Transfer, Credit Card, Cash, Cheque, etc.) - **Min/Max Amount** range - **Paid From/To** date range --- ## Chat Assistant The chat assistant (accessible via the "Chat" menu item) exposes **54 tools** the AI can call to answer property-management questions and create records. These include tools for: - Property counts, details, and comparisons (by status, structure, amenities, city, expenses) - Ticket summaries, severity breakdowns, properties with open tickets, and status/severity filtered lists - Lease summaries and lease renewal tracking - Expense summaries by category, by property, by status, top-expense property - Recurring expense summaries and recurring expenses by status - Invoice and payment summaries - Vendor summaries and vendor-type listings - Portfolio overview and revenue snapshots **Write tools — create records from chat:** The assistant can create prospects, vendors, tickets, expenses, properties, and tenants. Every create requires a confirmation: the assistant first tells you what it plans to create and asks for your go-ahead, and nothing is written until you confirm. Creating a property from chat uses minimal fields (name + address) and asks you to finish the details in the app. The same permission rules as the app apply (Company Admins can create anywhere; role 4 users must manage the property; the free-plan 5-property limit applies). **Newer tools added:** | Tool | Description | |------|-------------| | `get_overdue_invoices` | Lists every overdue invoice with tenant name, property name, due date, amount, and period | | `get_leases_ending_soon` | Lists active leases expiring within N days (default 30), with tenant, property, end date, and rent | | `get_tickets_by_severity` | Lists tickets filtered by severity (e.g. Critical, High) with property, description, and status | | `get_tickets_by_status` | Lists tickets filtered by status name and optionally severity name (e.g. "Open", "Critical") | | `get_revenue_summary` | Returns collected payments total vs outstanding (overdue + pending) invoice totals | | `get_property_with_most_expenses` | Returns the single property with the highest total expenses, including amount and count | | `get_property_with_most_running_expenses` | Returns the single property with the highest total running/recurring expenses | | `get_properties_with_open_tickets` | Lists all properties with open tickets, grouped by property with counts | | `get_expenses_by_status` | Lists expenses grouped by property, filtered by status name (e.g. "Paid", "Pending") | | `get_running_expenses_by_status` | Lists recurring expenses grouped by property, filtered by status name | The chat also supports a **WebSocket endpoint** (`/api/ws/chat` with token query param) for real-time streaming responses in addition to the existing SSE endpoint. Each user is limited to **100 chat requests per day** (shared across both endpoints). Exceeding the limit returns an error message from the chat. The daily counter resets automatically at midnight UTC. When the AI assistant runs on the paid deepseek model, each company also has a **monthly AI budget** (default $3.50, configurable via `AI_MONTHLY_BUDGET_USD`). Usage is metered at worst-case deepseek rates and once the budget is reached the chat reports the monthly limit until the 1st of the next month; cached answers (FAQ or exact-match) still work, and local/cached answers cost nothing. **Newer tools added (continued):** | Tool | Description | |------|-------------| | `get_lease_by_tenant` | Find active lease for a tenant by first and/or last name | | `get_tickets_by_tenant` | Find maintenance tickets for properties leased by a tenant name | **Create tools added:** | Tool | Description | |------|-------------| | `create_prospect` | Create a prospect (name required; property/stage/email/phone optional) | | `create_vendor` | Create a vendor (company name required; vendor type/property optional) | | `create_ticket` | Create a maintenance ticket for a property (description + property required) | | `create_running_expense` | Create a running/recurring expense for a property and vendor | | `create_property` | Create a property from minimal fields (name + address); finish details in the app | | `create_tenant` | Create a tenant (name + phone/email) | **Write confirmation flow:** All `create_*` tools are write-protected. The first time the AI calls one, the server replies with a confirmation prompt and writes nothing; only a second call with `confirm: true` (after the user agrees) actually creates the record. **Conceptual questions:** The assistant now has a `get_documentation` tool that searches the FAQ and User Experience docs to answer conceptual and how-to questions like "what is an invoice", "how does auto-billing work", or "what are the user roles". **System prompt rule:** If no database tool exists to answer, the assistant must say "I cannot answer that" — it must NOT compute averages, totals, comparisons, or estimates from results. **Entity resolution:** The assistant handles lowercase queries (e.g. "james smith") by falling back to individual non-stop-word tokens of 3+ characters when no capitalized name patterns are found. ### Single Chat History Each user has a single, persistent chat history (keyed by `user_`). There are no multiple conversations — no dropdown or "New Chat" button. The full message history is stored and loaded on each visit. The LLM context is compacted to the last 30 messages to stay within the model's window, but the database retains the complete history. ## Public Page (Marketplace) Companies can showcase their listings to the public (the **Marketplace**) with a shareable page that needs no login. - **Enable:** A Company Admin picks a public URL name in the company settings under Company Admin → Marketplace (verified unique before saving). The page is live at `/public/` (e.g. `/public/bavaria-immobilien`). Once set, the public name is **permanent** and cannot be changed or disabled. - **What shows:** Only properties with a "For Rent" or "For Sale" status that have the "Publicly listed" toggle on (it defaults to ON for new properties; turn it off per property to hide a listing). Properties with a "Sold" or "Rented" status are automatically de-listed. - **Privacy:** The public page shows only safe listing info — photos, name, city, price, size, beds/baths, parking. It never exposes street address, GPS, national IDs, documents, or internal notes. Public pages render no app chrome and no role/admin strings. - **Leads:** A "Contact agency" form on the page captures name, email, phone, message, and a GDPR consent checkbox. Each submission creates a prospect in the company's CRM (stage "new", source "public-website", next-follow-up set to now) so it appears in the "Upcoming Follow-ups" list immediately. - **Country pages (Phase B):** `/public/countries/` (e.g. `/public/al`) aggregates public listings from all opted-in companies in a country. A countries index at `/public/countries` lists every country with public listings (flag, name, count) and links to each country page; the landing page hero links to it. - **Search:** Both the company page and country page have a search bar in the hero. On a company page it searches only that company's listings; on a country page it searches only that country's listings. The rent/sale chips filter the same scoped set. - **Header:** Public pages show a light logo (link back to the landing page), a country switcher with flag + listing count (country pages only), and a language menu (English, Deutsch, Shqip, Hrvatski, Türkçe, ქართული, Čeština, Srpski) that re-labels the page instantly. - **Back to country:** Company pages show a "Back to " pill above the hero title that returns to the country page for that listing's country, so visitors who land on an agency page from a country listing can navigate back. - **Property detail pages:** Clicking any property card on a company or country page opens a dedicated detail page at `/public/property/` with a photo carousel at the top, the price, an overview grid (property type, bedrooms, bathrooms, size, floor, parking, elevator), the description, and breadcrumbs (Home → Browse properties by country → Country → Agency → Property). A clear **"I'm interested"** button opens the same contact form as the agency page (creates a CRM prospect). The page uses the same privacy rules — no street address, GPS, national IDs, or documents. The listing's status badge (e.g. "For Rent") is shown in the visitor's language via the language menu. - **Hero fade:** The bottom of the hero image fades smoothly into the page background so the transition to the property grid is seamless. - **Not-found states:** A missing agency slug, an unknown country code, or a removed property each render a clear, translated "not found" message (not the search's "no matches" text), so a typo'd or outdated URL never looks like an empty-but-valid page. - **AI assistant:** The chat assistant can tell you your public page URL and how many listings it shows (`get_public_page_url`). --- ## Bug Reports Any logged-in user can report a problem with the site: - **Web**: click the bug icon in the top bar → "Report a Problem" page. Enter a title and description, pick a category (Bug / Feature request / Other) and an urgency (Low / Medium / High / Critical). The current page is attached automatically. The same page lists "My Reports" (the user's own submissions with status/urgency chips). - **Mobile**: Settings → Report a Problem (same fields), and Settings → My Bug Reports lists submissions. - **Visibility**: a user sees only their own reports; Company Admins see all reports for their company. - **Triage**: Company Admins use the "Bug reports" screen (list → open → change status/urgency) or `PUT /api/bug-reports/:id`. - **Comment thread**: click a row in the Bugs list to open a dialog with the full conversation (original description + every comment with author/date) and a reply box. Anyone with access to the bug can reply (submitter, their company admin). The table shows a comment count per bug as the in-app signal that there's activity. Replies are blocked once the bug is closed. Web only — mobile stays read-only. - **Reporting**: `GET /api/bug-reports/report` (Company Admin only) summarizes open bugs by urgency and by age, plus totals by status. --- ## Development Policy: New Features & Fields When adding a new feature or model field, the API, UI, and docs must be updated together: - **API** (`./api/controllers/`): Add searchable query parameters to the relevant list endpoint so the new field can be filtered via `?field=value` or `?field_min=...&field_max=...` range params. - **UI** (`./ui/`): Add the corresponding filter controls (dropdowns, range sliders, text inputs) to the page's filter card or data table search area, following existing patterns. - **Docs**: Update `API-Guide.md`, `FAQ.md`, and `UserExperience.md` to reflect the change. This guarantees parity between what the API can query, what the user can filter in the interface, and what is documented. --- ## Invoice-Gen Cron Service (auto-billing) A Go-based cron job (`invoice-gen/`) runs nightly in a Docker container. It: - Finds all active leases with `auto_billing: true` where `start-date ≤ now ≤ end-date` - Calculates completed billing periods based on the lease's `period` (14, 30, 45, 60, or 90 days) - Creates a "Pending" invoice for each completed period, deduplicating by `period-month` (YYYY-MM) - Is idempotent: a second run produces zero new invoices for periods already billed # Property Portal API Guide Base URL: `http://:/api` (default port `8080`) --- ## Authentication ### POST `/api/login` — Public Authenticate a user and receive a JWT token. **Request:** ```json { "email": "string", "password": "string" // SHA-256 hash, NOT plain text } ``` > ⚠️ The password must be sent as a **SHA-256 hex digest** of the actual password, not the raw password. The frontend hashes it before sending; API consumers must do the same. **Errors:** - `403 Forbidden` — account exists but email is not confirmed yet (`confirmed: false`). Confirm via `/api/verify` first. **Response:** ```json { "user": { /* full User object */ }, "token": "jwt_string" } ``` --- ### POST `/api/signup` — Removed The old one-call signup (create user + optional company) was removed. The flow is now three steps: `POST /api/users` (creates an unconfirmed user + emails a 6-digit code), `POST /api/verify` (confirms), `POST /api/company/setup` (creates the company + starts the 30-day trial). --- ### POST `/api/users` — Public Create a new user. The user is created **unconfirmed**: a 6-digit confirmation code and a magic link are sent by email (`ConfirmCode`/`ConfirmExpires` set). Returns `{ id, message }` — no token until confirmed. **Request:** ```json { "email": "string", "password": "string", "first_name": "string", "last_name": "string" } ``` **Response (200):** ```json { "id": "ObjectID", "message": "Confirmation code sent to email" } ``` --- ### POST `/api/verify` — Public Confirm an email address with the code from the confirmation email. Both code entry and magic-link confirmation use this endpoint. **Request:** ```json { "email": "string", "code": "string" } ``` **Response (200):** ```json { "message": "Email confirmed. You can now sign in." } ``` --- ### POST `/api/resend-confirm` — Public Re-send the 6-digit confirmation code to an unconfirmed user. **Request:** ```json { "email": "string" } ``` --- ### POST `/api/company/setup` — Auth Create the user's company and start the free plan + 30-day trial. Called by the onboarding wizard after a user confirms their email and signs in. `company_name` defaults to "First Last" if omitted; `company_logo` (optional base64 data URL) is saved to disk. Returns a fresh JWT carrying the new `company_id`. **Request:** ```json { "company_name": "string | null", "company_website": "string | null", "company_logo": "string | null" } ``` **Response (200):** ```json { "token": "JWT", "user": { "...": "user" }, "company": { "id": "ObjectID", "name": "string", "plan": "free", "trial_ends_at": "ISO8601", "subscribed": false } } ``` --- ## Authorization ### JWT Token All protected endpoints require: `Authorization: Bearer ` ### Roles | ID | Name | Scope | |----|------|-------| | 1 | Super Admin | Full access, all logs | | 2 | Admin | Full access | | 3 | Company Admin | Own company CRUD, can create users | | 4 | Company User | Read company data, create entities | --- ## Data Models ### User ```json { "id": "ObjectID", "email": "string", "first_name": "string", "last_name": "string", "tel_1": "string | null", "tel_2": "string | null", "company_id": "ObjectID", "company_address_id": "ObjectID | null", "password": "string", "active": true, "notes": "string | null", "create_date": "ISO8601", "last_login": "ISO8601", "confirmed": true, "confirm_code": "string | null", "confirm_expires": "ISO8601 | null", "role_id": 1, "language": "string | null", "ps_theme": "string | null", "managed_properties": [ { "property_id": "ObjectID", "created_date": "ISO8601", "created_by": "ObjectID", "active": true } ] } ``` ### Property ```json { "id": "ObjectID", "name": "string", "company_id": "ObjectID", "property_status_id": "ObjectID | null", "status_name": "string", "can_edit": true, "structure_type_id": "ObjectID | null", "description": "string | null", "create_date": "ISO8601", "created_by": "ObjectID", "last_updated": "ISO8601", "last_updated_by": "ObjectID", "list_publicly": true | false, "compliance_enabled": true | false | null, "country_code": "string", "property_address": { "address_line_1": "string", "address_line_2": "string | null", "city": "string", "postal_code": "string | null", "state": "string | null", "country": "string | null", "gps": "string | null", "national_id": "string | null", "notes": "string | null" }, "property_details": { "square_meters": 0.0, "bedrooms": 0, "total_rooms": 0, "bathrooms": 0, "floor_number": 0, "elevator_count": 0, "parking_spaces": 0 }, "property_photos": [ { "name": "string", "description": "string | null", "photo": "data:image/jpeg;base64,...", "cover_photo": true | false, "create_date": "ISO8601", "created_by": "ObjectID" } ], "rental_details": { "rental_price": 0.0, "currency_id": "ObjectID", "rented_date": "ISO8601 | null", "available_date": "ISO8601 | null", "with_contract": true | false }, "purchase_details": { "purchase_price": 0.0, "currency_id": "ObjectID", "purchase_details": "string | null", "purchase_status_id": "ObjectID | null", "purchase_date": "ISO8601 | null", "notes": "string | null" }, "sale_details": { "asking_price": 0.0, "sold_price": 0.0, "currency_id": "ObjectID | null", "status_id": "ObjectID | null", "notes": "string | null" }, "property_documents": [ { "name": "string", "link": "data:;base64,...", "file_extension": "string", "description": "string | null", "create_date": "ISO8601" } ], "compliance_certs": [ { "id": "ObjectID", "type": "string", "number": "string", "issued_date": "ISO8601 | null", "expiry_date": "ISO8601 | null", "status": "valid | expiring | expired", "link": "data:;base64,...", "file_extension": "string", "notes": "string | null", "create_date": "ISO8601", "created_by": "ObjectID" } ] } ``` ### Expense ```json { "id": "ObjectID", "property_id": "ObjectID", "category_id": "ObjectID", "currency_id": "ObjectID", "amount": 0.0, "expense_date": "ISO8601", "expense_status_id": "ObjectID", "status_name": "string", "notes": "string | null", "receipts": [ { "name": "string", "link": "data:;base64,...", "file_extension": "string", "description": "string | null", "create_date": "ISO8601" } ], "expense_reference": "string | null", "timestamp": "ISO8601", "created_by": "ObjectID" } ``` ### Tenant ```json { "id": "ObjectID", "company_id": "ObjectID", "first_name": "string", "last_name": "string", "email": "string | null", "phone": "string | null", "active": true, "create_date": "ISO8601", "created_by": "ObjectID", "tenant_documents": [ { "name": "string", "link": "data:;base64,...", "file_extension": "string", "description": "string | null", "create_date": "ISO8601" } ] } ``` ### Prospect ```json { "id": "ObjectID", "company_id": "ObjectID", "first_name": "string", "last_name": "string", "email": "string | null", "phone": "string | null", "property_id": "ObjectID | null", "stage": "new | contacted | viewing | offer | closed-won | closed-lost", "next_follow_up": "ISO8601 | null", "notes": [ { "note": "string", "created_by": "ObjectID", "create_date": "ISO8601" } ], "active": true, "create_date": "ISO8601", "created_by": "ObjectID", "converted_tenant_id": "ObjectID | null", "estimated_price": "number | null", "deal_price": "number | null", "commission_rate": "number | null", "commission_type": "percent | months | fixed" } ``` ### Lease ```json { "id": "ObjectID", "property_id": "ObjectID", "property_name": "string", "tenant_id": "ObjectID", "company_id": "ObjectID", "start_date": "ISO8601", "end_date": "ISO8601", "currency_id": "ObjectID", "rent": 0.0, "deposit": 0.0, "period": 0, "auto_billing": false, "active": true, "lease_documents": [ { "name": "string", "link": "data:;base64,...", "file_extension": "string", "description": "string | null", "create_date": "ISO8601" } ], "create_date": "ISO8601", "created_by": "ObjectID" } ``` ### Invoice ```json { "id": "ObjectID", "company_id": "ObjectID", "lease_id": "ObjectID", "currency_id": "ObjectID", "due_date": "ISO8601", "total_amount": 0.0, "period_month": "string", "invoice_status_id": "ObjectID", "status_name": "string", "notes": "string | null", "create_date": "ISO8601", "created_by": "ObjectID" } ``` ### Payment ```json { "id": "ObjectID", "invoice_id": "ObjectID", "currency_id": "ObjectID", "amount": 0.0, "payment_date": "ISO8601", "method_id": "ObjectID", "notes": "string | null", "create_date": "ISO8601", "created_by": "ObjectID" } ``` ### Vendor ```json { "id": "ObjectID", "name": "string", "description": "string | null", "vendor_type_id": "ObjectID", "property_id": "ObjectID", "account": "string | null", "tel_1": "string | null", "tel_2": "string | null", "email": "string | null", "website": "string | null", "contact": "string | null", "company_id": "ObjectID", "active": true, "create_date": "ISO8601", "created_by": "ObjectID" } ``` ### Running Expense ```json { "id": "ObjectID", "property_id": "ObjectID", "vendor_id": "ObjectID", "invoice_number": "string", "currency_id": "ObjectID", "amount": 0.0, "status_id": "ObjectID", "status_name": "string", "billing_period": "string", "company_id": "ObjectID", "receipts": [ { "name": "string", "link": "data:;base64,...", "file_extension": "string", "description": "string | null", "create_date": "ISO8601" } ], "create_date": "ISO8601", "created_by": "ObjectID" } ``` ### Ticket ```json { "id": "ObjectID", "property_id": "ObjectID", "property_name": "string", "description": "string", "status_id": "ObjectID", "status_name": "string", "severity_id": "ObjectID", "severity_name": "string", "created_date": "ISO8601", "created_by": "ObjectID", "created_by_name": "string", "due_date": "ISO8601", "resolved_date": "ISO8601 | null", "last_updated": "ISO8601", "last_updated_by": "ObjectID" } ``` ### Company ```json { "id": "ObjectID", "name": "string", "description": "string | null", "logo": "string | null", "active": true | false, "company_address": [ { "address_id": "ObjectID", "name": "string", "address_line_1": "string", "address_line_2": "string | null", "city": "string", "postal_code": "string | null", "state": "string | null", "country": "string | null", "gps": "string | null", "national_id": "string | null", "notes": "string | null", "website": "string | null", "contact": ["string", "..."] } ], "create_date": "ISO8601", "created_by": "ObjectID", "public_slug": "string", "plan": "free | pro", "trial_ends_at": "ISO8601 | null", "subscribed": false } ``` --- ## Endpoints ### Properties #### POST `/api/properties` — Auth Create a new property. Photos and documents can be included as base64 data URLs and are saved to disk automatically. **Request:** Full `Property` JSON. Photos in `property_photos[].photo` as `data:image/...;base64,...`. Documents in `property_documents[].link` as `data:...;base64,...`. **Note:** `property_address.country` is **required** — a `400` is returned when it is missing or blank. This also applies to `PUT /api/properties/:id` when a `property_address` is submitted. The `country_code` field is derived server-side from it. **Note:** Properties default to `list_publicly: true` unless explicitly opted out. Properties whose status resolves to **Sold** or **Rented** are always de-listed from the Marketplace — `list_publicly` is forced to `false` regardless of the submitted value. **Errors:** - `400 Bad Request` — `property_address.country` is missing or blank - `402 Payment Required` — company is on the free plan without an active trial and already holds the free-tier limit of 5 properties. UI shows the upgrade prompt. #### GET `/api/properties` — Auth List properties. Scope depends on role: - Role 3: company properties - Role 4: managed properties only **Query params:** Top-level fields: - `?property_status_id=` — filter by property status ID - `?status=` — filter by property status slug (e.g. `for-rent`, `vacant`) - `?structure_type_id=` — filter by structure type - `?created_by=` — filter by creator - `?create_date_from=` — created on or after - `?create_date_to=` — created on or before - `?list-publicly=true|false` — filter by the public listing toggle (`true` also matches properties where the field is unset) - `?compliance-enabled=true|false` — filter by the per-property compliance opt-in (`true` matches only properties with tracking enabled; `false` matches all others, including unset) PropertyAddress (nested): - `?address_line_1=` — regex match - `?address_line_2=` — regex match - `?city=` — regex match - `?postal_code=` — regex match - `?state=` — regex match - `?country=` — regex match - `?gps=` — regex match PropertyDetails (nested): - `?square_meters_min=` / `?square_meters_max=` — range - `?bedrooms=` — exact match - `?total_rooms=` — exact match - `?bathrooms=` — exact match - `?floor_number=` — exact match - `?elevator=` — `true` (elevator-count ≥ 1) or `false` (elevator-count = 0) - `?elevator_count=` — exact match - `?private_parking=` — `true` (parking-spaces ≥ 1) or `false` (parking-spaces = 0) - `?parking_spaces=` — exact match RentalDetails (nested): - `?rental_price_min=` / `?rental_price_max=` — range - `?rental_currency_id=` — filter by currency - `?rented_date_from=` / `?rented_date_to=` — range - `?available_date_from=` / `?available_date_to=` — range - `?with_contract=` — `true` or `false` PurchaseDetails (nested): - `?purchase_price_min=` / `?purchase_price_max=` — range - `?purchase_currency_id=` — filter by currency - `?purchase_status_id=` — filter by purchase status - `?purchase_date_from=` / `?purchase_date_to=` — range SaleDetails (nested): - `?asking_price_min=` / `?asking_price_max=` — range - `?sold_price_min=` / `?sold_price_max=` — range - `?sale_currency_id=` — filter by currency - `?sale_status_id=` — filter by sale status **Response:** Array of `Property` #### GET `/api/properties/active` — Auth Returns all properties for the JWT's company. **Response:** Array of `Property` #### GET `/api/properties/:id` — Auth Get a single property by ID. **Response:** `Property` — includes `can_edit` (bool), computed server-side from the caller's role and managed properties (true for roles 1-3 on company properties, and for role 4 on properties they manage). #### PUT `/api/properties/:id` — Auth Update property fields. Re-processes photos and documents if provided. Allowed for roles 1-3 (company-scoped) and role 4 on properties they manage. **Note:** If the submitted `property_status_id` resolves to **Sold** or **Rented**, `list_publicly` is forced to `false` — the property is automatically de-listed from the Marketplace. #### POST `/api/properties/:id/photos` — Auth Upload photos via multipart form. Accepts multiple files in the `photo` field. Images are resized to 1200px max and saved as JPG in `images/`. #### DELETE `/api/properties/:id` — Auth (role 1-3) Hard delete property. Company-scoped. ### Compliance Compliance tracking is a **per-property opt-in** (`compliance_enabled` on the property; default off). Properties that have not opted in are invisible to compliance — no summary rows, no expiring-cert output, no cert writes. Stored certs are never deleted on toggle-off. Compliance certificates are embedded in the `compliance_certs` array of a property. Certificate `status` (`valid` / `expiring` / `expired`) is derived server-side from the expiry date and the property's market rule (derived from the company's address country). Statuses are advisory. Supported markets (with a required-certificate list): Turkey (`tr`), Georgia (`ge`), Czechia (`cz`), Serbia (`rs`), Moldova (`md`), Albania (`al`), Kosovo (`xk`). Other countries still support cert tracking, just no required list. #### GET `/api/compliance/summary` — Auth Aggregate compliance state for the caller's company (optionally a single property via `?property_id=`). Role-4 users only see their managed properties. Only opted-in properties are included. **Response:** array of `{property_id, name, address, market, total, valid, expiring, expired, missing[]}` — `missing` is the list of required cert types (for supported markets) that the property does not yet have. #### GET `/api/compliance/rules?property_id=` — Auth Returns the required-certificate rule set for a property's market, used to guide users when adding certificates. The market is derived from the property's address country, falling back to the company's market country. **Response:** `{country_code, country_name, required_certs[], renewal_notice_days, guidance, last_reviewed, source_url}`. `required_certs` is an array of `{type, label, renewal_note}`. When the property's market has no rule set, an empty rule object is returned (all fields empty/zero). #### POST `/api/compliance/certs` — Auth Add a certificate to a property. Returns `400` with an error message if the property has not opted into compliance tracking. ```json { "property_id": "ObjectID", "cert": { "type": "energy-passport", "number": "REF-123", "issued_date": "2026-01-01", "expiry_date": "2036-01-01", "link": "data:application/pdf;base64,...", "notes": "optional" } } ``` `link` may be a base64 data URL; it is saved to `./files/compliance/` and replaced with the stored path. **Response:** the created certificate. #### PUT `/api/compliance/certs/:id` — Auth Update a certificate. Body is `{property_id, cert:{...}}` (same shape as create). Returns `400` if the property has not opted into compliance tracking. **Response:** the updated certificate. #### DELETE `/api/compliance/certs/:id` — Auth Remove a certificate. Body: `{"property_id": "ObjectID"}`. Returns `400` if the property has not opted into compliance tracking. The stored file (if any) is deleted from disk. **Response:** `{"message": "..."}`. All compliance write endpoints require access to the target property (company-scoped for roles 1-3, managed-only for role 4). --- ### Expenses #### POST `/api/expenses` — Auth Create an expense. Receipts in `receipts[].link` as base64 data URLs are saved to `./files/receipts`. **Query params:** None #### GET `/api/expenses` — Auth List expenses. **Query params:** - `?property_id=` — filter by property - `?category_id=` — filter by expense category - `?currency_id=` — filter by currency - `?expense_status_id=` — filter by expense status ID - `?status=` — filter by expense status slug (e.g. `paid`, `pending`) - `?created_by=` — filter by creator - `?amount_min=` / `?amount_max=` — amount range - `?expense_date_from=` / `?expense_date_to=` — date range **Response:** Array of `Expense` with receipt links as data URLs #### GET `/api/expenses/:id` — Auth Get a single expense. **Response:** `Expense` #### PUT `/api/expenses/:id` — Auth Update expense. Blocked if status is "paid". Only creator or admin. #### DELETE `/api/expenses/:id` — Auth (role 1-3) Hard delete expense. Blocked if status is "paid". --- ### Tenants #### POST `/api/tenants` — Auth Create a tenant. Documents saved to `./files/tenant-documents`. Provide documents as base64 in `tenant_documents[].link`. #### GET `/api/tenants` — Auth List tenants, company-scoped. Scope depends on role: - Role 3 (Company Admin) and above: all company tenants - Role 4 (User): only tenants linked (via leases) to the caller's managed properties **Query params:** - `?name=` — regex match against first or last name - `?email=` — regex match - `?phone=` — regex match - `?active=` — filter by active status. Defaults to `true` when omitted. Pass `all` to skip active filtering. **Response:** Array of `Tenant` #### GET `/api/tenants/:id` — Auth Get a single tenant. Scope matches the list: role 4 callers may only read tenants linked to their managed properties (403 `Access denied` otherwise). #### PUT `/api/tenants/:id` — Auth Update tenant. Re-processes documents. #### DELETE `/api/tenants/:id` — Auth Soft-delete (sets `active: false`). --- ### Prospects (CRM) #### POST `/api/prospects` — Auth Create a prospect. Requires company access; role 4 (Company User) must also manage the prospect's property (403 otherwise). **Request:** ```json { "first_name": "string", "last_name": "string", "email": "string | null", "phone": "string | null", "property_id": "ObjectID | null", "stage": "new | contacted | viewing | offer | closed-won | closed-lost", "source": "string (e.g. Zillow, Referral, Open House)", "tags": ["string"], "next_follow_up": "ISO8601 | null", "notes": [ { "type": "call | email | meeting | note", "note": "string", "created_by": "ObjectID", "create_date": "ISO8601" } ], "tasks": [ { "title": "string", "due_date": "ISO8601 | null", "done": "boolean" } ], "estimated_price": "number | null", "commission_rate": "number | null", "commission_type": "percent | months | fixed" } ``` > **Notes (interaction log):** the `type` field marks each entry as a `call`, `email`, `meeting`, or plain `note`. Legacy entries without a type render as `note`. > > **Tasks:** a prospect can carry multiple tasks with a title, optional due date, and a done flag. Passed wholesale on `PUT`; a partial update that omits `tasks` (e.g. the mobile form) preserves the existing tasks. #### GET `/api/prospects` — Auth List prospects. Scoped: Company Admin sees their company; Company User (role 4) sees only prospects for properties they manage. **Query params:** - `?name=` — regex match against first or last name - `?email=` — regex match - `?phone=` — regex match - `?stage=` — filter by stage - `?source=` — filter by lead source (exact match) - `?tag=` — filter by tag (exact match, prospects containing the tag) - `?property_id=` — filter by property - `?tasks_due=` — only prospects whose `tasks` array contains a matching done state (`true` = all their tasks are done; `false` = has at least one open task) - `?overdue=true` — only prospects with at least one open task whose due date is in the past - `?active=` — filter by active status. Defaults to `true`. **Response:** Array of `Prospect` #### GET `/api/prospects/:id` — Auth Get a single prospect (same scoping rules). #### PUT `/api/prospects/:id` — Auth Update prospect fields. Writes require the existing record to be manageable (403 otherwise). #### DELETE `/api/prospects/:id` — Auth Soft-delete (sets `active: false`). #### POST `/api/prospects/:id/convert` — Auth Convert a prospect to a tenant: sets stage `closed-won`, creates a `Tenant` from the prospect's contact details (same company), and stores `converted-tenant-id` on the prospect. Role 4 must manage the prospect's property. **Response:** the created `Tenant` #### POST `/api/prospects/:id/enroll` — Auth Enroll a prospect in a follow-up sequence. Creates the first task from the sequence's first step. **Request:** ```json { "sequence_id": "ObjectID" } ``` #### POST `/api/prospects/:id/unenroll` — Auth Remove the prospect's sequence enrollment (clears `sequence-id` and `sequence-step`). #### POST `/api/prospects/:id/advance-sequence` — Auth Advance the prospect to the next step in their enrolled sequence and create the next task. If the prospect is already on the last step, the enrollment is cleared (sequence completed). ### Follow-Up Sequences #### POST `/api/follow-up-sequences` — Auth Create a follow-up sequence. Company-scoped. **Request:** ```json { "name": "string", "description": "string", "steps": [ { "title": "string", "day_offset": 0, "note_type": "call | email | meeting | note" } ] } ``` #### GET `/api/follow-up-sequences` — Auth List all active sequences for the user's company. #### GET `/api/follow-up-sequences/:id` — Auth Get a single sequence by ID. #### PUT `/api/follow-up-sequences/:id` — Auth Update a sequence's name, description, and steps. #### DELETE `/api/follow-up-sequences/:id` — Auth Soft-delete (sets `active: false`). **Reports:** `GET /api/reports` includes a `prospects` block: ```json "prospects": { "by_stage": { "new": 3, "contacted": 5, "viewing": 2, "offer": 1, "closed-won": 4, "closed-lost": 2 }, "summary": { "total": 17, "open": 11, "won": 4, "lost": 2, "conversion_rate": 0.667 }, "forecast": { "weighted_commission": 3250, "by_stage": { "new": 20, "contacted": 60 } }, "follow_ups": [ { "id": "ObjectID", "name": "Jane Doe", "stage": "viewing", "next_follow_up": "ISO8601 | null", "open_tasks": 2 } ] } ``` `follow_ups` lists open prospects that need attention: a `next_follow_up` within the next 7 days or any open task. Sorted by soonest follow-up date (task-only entries last), capped at 20. Weighted forecast = Σ per open prospect `commission_amount(estimated_price) × stage_probability`, with fixed probabilities (new 10%, contacted 25%, viewing 50%, offer 75%). Scoped to the same role rules as the list endpoint. --- ### Leases #### POST `/api/leases` — Auth Create a lease. Documents saved to `./files/lease-documents`. Provide documents as base64 in `lease_documents[].link`. #### GET `/api/leases` — Auth List leases, company-scoped. **Query params:** - `?property_id=` — filter by property - `?tenant_id=` — filter by tenant - `?active=` — filter by active status. Defaults to `true` when omitted (backward-compatible). Pass `all` to skip active filtering. - `?auto_billing=` — filter by auto-billing - `?start_date_from=` / `?start_date_to=` — start date range - `?end_date_from=` / `?end_date_to=` — end date range - `?rent_min=` / `?rent_max=` — rent amount range **Response:** Array of `Lease` — each lease includes a resolved `property_name` read from the referenced property document. #### GET `/api/leases/:id` — Auth Get a single lease. Response includes resolved `property_name`. #### PUT `/api/leases/:id` — Auth Update lease. Re-processes documents. #### DELETE `/api/leases/:id` — Auth (role 1-3) Soft-delete (sets `active: false`). Company-scoped. --- ### Invoices #### POST `/api/invoices` — Auth Create an invoice. #### GET `/api/invoices` — Auth List invoices. Scope depends on role: - Role 3 (Company Admin) and above: all company invoices - Role 4 (User): only invoices on leases for the caller's managed properties **Query params:** - `?lease_id=` — filter by lease - `?period_month=` — regex match (e.g. `2024-01`) - `?invoice_status_id=` — filter by invoice status ID - `?status=` — filter by invoice status slug (e.g. `paid`, `overdue`) - `?total_amount_min=` / `?total_amount_max=` — amount range - `?due_date_from=` / `?due_date_to=` — due date range - `?create_date_from=` / `?create_date_to=` — creation date range - `?notes=` — regex match #### GET `/api/invoices/:id` — Auth Get a single invoice. Scope matches the list: role 4 callers may only read invoices on leases for their managed properties (403 `Access denied` otherwise). #### GET `/api/invoices/outstanding/:id` — Auth Returns invoices with status "Pending" or "Overdue" for a given company ID, scoped to the caller (role 4 sees only their managed properties). #### GET `/api/invoices/company/:id` — Auth Returns invoices for an company with pagination. **Query params:** - `?page=` — page number (default 1, 50 per page) - `?lease_id=` — filter by lease - `?period_month=` — regex match - `?invoice_status_id=` — filter by invoice status ID - `?status=` — filter by invoice status slug (e.g. `paid`, `overdue`) - `?total_amount_min=` / `?total_amount_max=` — amount range - `?due_date_from=` / `?due_date_to=` — due date range - `?create_date_from=` / `?create_date_to=` — creation date range - `?notes=` — regex match **Response:** ```json { "invoices": [ /* Invoice[] */ ], "total": 100, "page": 1, "pages": 2 } ``` #### GET `/api/invoices/:id/pdf` — Auth Download a PDF version of an invoice. Generates an A4 PDF on the fly with the Property Sage branding, invoice details (ID, lease, property, due date, period, amount, currency, status), and the company name. **Response:** `application/pdf` binary with `Content-Disposition: attachment`. #### GET `/api/invoices/pdf-by-lease/:leaseId` — Auth Finds the most recent invoice for the given lease and returns its PDF. Same output format as `GET /api/invoices/:id/pdf`. **Response:** `application/pdf` binary with `Content-Disposition: attachment`. #### PUT `/api/invoices/:id` — Auth Update invoice fields. #### DELETE `/api/invoices/:id` — Auth (role 1-3) Hard delete. Company-scoped. --- ### Payments #### POST `/api/payments` — Auth Create a payment. Automatically sets the related invoice status to "Paid". #### GET `/api/payments` — Auth List payments. Role 1/2 sees all; others see payments linked to their company's invoices. **Query params:** - `?invoice_id=` — filter by invoice - `?method_id=` — filter by payment method - `?currency_id=` — filter by currency - `?created_by=` — filter by creator - `?amount_min=` / `?amount_max=` — amount range - `?payment_date_from=` / `?payment_date_to=` — payment date range #### GET `/api/payments/:id` — Auth Get a single payment. #### GET `/api/payments/:id/detail` — Auth Get enriched payment detail. **Response:** ```json { "property_name": "string", "tenant_name": "string", "method": "string", "payment_date": "string", "notes": "string" } ``` #### PUT `/api/payments/:id` — Auth Update payment fields. #### DELETE `/api/payments/:id` — Auth (role 1-3) Hard delete. Resets invoice status to "Pending". --- ### Vendors #### POST `/api/vendors` — Auth Create a vendor. #### GET `/api/vendors` — Auth List active vendors, company-scoped. **Query params:** - `?name=` — regex match - `?vendor_type_id=` — filter by vendor type - `?property_id=` — filter by property - `?account=` — regex match - `?tel_1=` — regex match on phone 1 - `?tel_2=` — regex match on phone 2 - `?email=` — regex match on email - `?website=` — regex match on website - `?contact=` — regex match on contact person - `?create_date_from=` / `?create_date_to=` — creation date range #### GET `/api/vendors/:id` — Auth Get a single vendor. #### PUT `/api/vendors/:id` — Auth Update vendor fields. #### DELETE `/api/vendors/:id` — Auth (role 1-3) Soft-delete (sets `active: false`). Company-scoped. --- ### Expenses (unified — one-off + recurring) The `expenses` collection is the single home for every cost record. Recurring records (the former "running expenses") carry `recurring: true` plus an optional period/vendor; records generated by an automatic-expense template carry `template-id`, `generated: true` and an "auto" badge in the UI. #### POST `/api/expenses` — Auth Create an expense. Receipts saved to `./files/receipts`. Body fields: `property_id` (required), `category_id`, `currency_id` (defaults to company currency), `amount` (>0), `expense_date`, `expense_status_id`, `notes`, `expense_reference`, `receipts`. Recurring records additionally accept `recurring: true`, `period` (`"2026-08"`), `vendor_id`, `invoice_number`, `paid_by_tenant`, `due_date`, `template_id` (immutable after creation for generated records), `amount_mode` (`"one-off"` | `"recurring"` | `"fixed"` | `"variable"`). > ⚠️ **Auto-invoice generation:** If `paid_by_tenant` is set to `true`, the server automatically creates a "Pending" invoice. It looks up the active lease for the property, resolves the vendor/vendor-type names, and populates `Notes` as `"Vendor.Name, VendorType.Name, InvoiceNumber"`. The invoice's `DueDate` is set to 15 days from now. The invoice carries `source: "template"` and `expense-id` pointing back at the expense. #### GET `/api/expenses` — Auth List expenses, company-scoped. **Query params:** - `?property_id=` — filter by property - `?category_id=` — filter by category - `?currency_id=` — filter by currency - `?status=` — filter by expense status slug (e.g. `paid`, `pending`) - `?expense_status_id=` — filter by expense status ID - `?recurring=` — filter by recurring flag - `?template_id=` — filter to records from one automatic-expense template - `?period=` — regex match on period - `?amount_min=` / `?amount_max=` — amount range - `?expense_date_from=` / `?expense_date_to=` — date range - `?created_by=` — filter by creator **Response additions:** every expense includes `recurring`, `period`, `due_date`, `generated`, `vendor_id`, `invoice_number`, `paid_by_tenant`, `amount_mode`, plus joined `property_name`, `vendor_name`, and `template_name` (the template's category label) for generated records. #### GET `/api/expenses/:id` — Auth Get a single expense. #### PUT `/api/expenses/:id` — Auth Update. Blocked if status is "paid". Only creator or admin. Editing `amount` on a generated record overrides that period only and never touches the template. On a **Pending→Paid** transition, the entry's final amount is copied to the template's `last-actual-amount` (used as the next pre-fill for variable templates). #### DELETE `/api/expenses/:id` — Auth (role 1-3) Hard delete. Blocked if "paid". Company-scoped. --- ### Expense Templates (automatic expenses) Automatic expense templates are set-once recurring cost definitions (mortgage, insurance, service charge, electricity) attached to a property. The nightly `invoice-gen` job generates `expenses` records from active templates so nobody re-enters a cost every period. Templates are stored in the `expense_templates` collection. #### POST `/api/expense-templates` — Auth Create a template. Body fields: `property_id` (required), `vendor_id` (required), `category_id`, `amount` (>0, required), `amount_mode` (`fixed` | `variable`, required; variable = amount is the typical/last-known value), `currency_id` (defaults to company currency), `frequency` (`monthly` | `quarterly` | `yearly`, default `monthly`), `paid_by_tenant`, `start_date`, `end_date`, `due_day` (default = start date's day, clamped to month length), `notes`. Managed-scope roles can only create templates for properties they manage. #### GET `/api/expense-templates` — Auth List templates, company-scoped. Response joins `property_name`, `vendor_name`, `category_name`, `currency_code` and computes `next_period` + `next_due_date` from the last generated period. **Query params:** - `?property_id=` — filter by property - `?active=` — filter by active state #### GET `/api/expense-templates/:id` — Auth Get a single template (company-scope checked). #### PUT `/api/expense-templates/:id` — Auth Update a template. **Changes apply to future periods only** — existing records are never rewritten. `active: false` pauses generation; setting `end_date` stops it. Changing `amount` on a variable template only affects the next pre-fill/forecast. #### DELETE `/api/expense-templates/:id` — Auth (role 1-3) Soft delete by default (`active = false`, records remain as history). Hard delete only with `?hard=true` for God/Site admins. --- ### Tickets #### POST `/api/tickets` — Auth Create a ticket. For role 4, checks managed property access. #### GET `/api/tickets` — Auth List tickets. Response includes resolved `property_name`, `status_name`, `severity_name`, and `created_by_name`. **Query params:** - `?property_id=` — filter by property - `?status_id=` — filter by ticket status ID - `?status=` — filter by ticket status slug (e.g. `open`, `in-progress`) - `?severity_id=` — filter by severity - `?created_by=` — filter by creator - `?last_updated_by=` — filter by last updater - `?created_date_from=` / `?created_date_to=` — creation date range - `?due_date_from=` / `?due_date_to=` — due date range - `?resolved_date_from=` / `?resolved_date_to=` — resolved date range - `?last_updated_from=` / `?last_updated_to=` — last updated range #### GET `/api/tickets/open` — Auth Returns open tickets (status "Open"). Role 4 sees managed property tickets; others see own tickets. Response includes resolved `property_name`. #### GET `/api/tickets/:id` — Auth Get a single ticket. Response includes resolved `property_name`. #### PUT `/api/tickets/:id` — Auth Update ticket fields. #### DELETE `/api/tickets/:id` — Auth (role 1-3) Hard delete. Company-scoped via property lookup. --- ### Companies #### POST `/api/companies` — Admin Create company. #### GET `/api/companies` — Auth (role 1-3) List active companies. Optional query param `public-slug` filters by exact (case-insensitive) public slug. #### GET `/api/companies/:id` — Auth Get a single company. #### PUT `/api/companies/:id/public-slug` — Admin (Company Admin: own company only) Set the company's public page URL name for the Marketplace. Body: `{"slug": "bavaria-immobilien"}`. The slug is normalized (lowercased, diacritics stripped, `[a-z0-9-]` only). Reserved words return `400`; a slug already held by another company returns `409` with a `suggestion`. Company Admins may only set the public name of their own company; other companies return `403`. Once a public name is set it is permanent — a different value (or an empty string) returns `400` `{"error": "The public name is permanent and cannot be changed once set"}`. On success returns `{"public_slug": "..."}`. #### GET `/api/companies/:id/footprint` — Auth (company-scoped) Estimate a company's total data footprint. Company Admin may query their own company (403 otherwise). Returns the exact BSON byte size of the company's documents (`db_bytes`) plus the sum of the sizes of its uploaded files on disk (`file_bytes`), combined into `total_bytes` / `total_mb`. File sizes come from `os.Stat` of the referenced file paths — no stored counters. An `ai` object reports the company's metered chatbot usage (from `chat_ai_usage`, populated when `LLM_PROVIDER=deepseek`): per-month `prompt_tokens` / `completion_tokens` and worst-case `cost`, plus totals, the current month, and `over_budget` against the `budget` (`AI_MONTHLY_BUDGET_USD`). Costs use the same rates as the chat budget gate, so the report matches what is billed. **Response:** ```json { "company_id": "6a6ca7afd6cda0159e74e32b", "db_bytes": 54894, "file_bytes": 7083, "total_bytes": 61977, "total_mb": 0.059, "collections": [ { "name": "expenses", "bytes": 5032, "docs": 15, "file_bytes": 0 }, { "name": "properties", "bytes": 14808, "docs": 15, "file_bytes": 7083 } ], "ai": { "current_month": "2026-08", "over_budget": false, "budget": 3.50, "months": [ { "month": "2026-08", "prompt_tokens": 40000000, "completion_tokens": 2000000, "cost": 6.16 } ], "total_prompt_tokens": 40000000, "total_completion_tokens": 2000000, "total_cost": 6.16 } } ``` File references counted: expense/running-expense receipts, property photos and documents, tenant documents, lease documents, and the company logo. Missing files count as 0. #### GET `/api/companies/:id/pricing` — Auth Returns the plan catalog resolved to the company's **effective** billing currency (explicit `billing-currency` → majority country code of the company's properties → `EUR`), plus the currency info the payment processor will consume: ```json { "currency": "GBP", "billing_currency": "", "currencies_available": ["EUR", "GBP", "USD"], "plans": [ { "slug": "free", "name": "Free", "price_minor": 0, "currency": "GBP", "limits": { "properties": 5, "storage_bytes": 524288000 }, "trial_days": 30 }, { "slug": "pro", "name": "Pro", "price_minor": 1300, "currency": "GBP", "limits": { "properties": 0, "storage_bytes": 0 }, "trial_days": 30 } ] } ``` Prices are **minor units** (cents/pence). `trial_days` = 30. #### PUT `/api/companies/:id/billing-currency` — Auth (Company Admin: own company only) Set the company's explicit billing currency. Body: `{"currency": "GBP"}`. Valid values: `EUR`, `GBP`, `USD`. Company Admins may only set their own company's currency (403 otherwise). Returns the updated company. #### PUT `/api/companies/:id` — Admin Update company fields. #### DELETE `/api/companies/:id` — Admin Soft-delete (sets `active: false`). #### POST `/api/companies/:id/address` — Admin Add an address to the company. **Request:** `CompanyAddress` object. #### PUT `/api/companies/:id/address/:addressId` — Admin Update a specific company address. #### DELETE `/api/companies/:id/address/:addressId` — Admin Remove an address from the company. --- ### Users #### GET `/api/users` — Auth (role 1-3) List active users, company-scoped for role 3. #### POST `/api/users/by-email` — Auth (role 1-3) Find user by email. **Request:** ```json { "email": "string" } ``` #### GET `/api/users/company/:id` — Auth (role 3) List all users in an company (active and inactive). Checks company ID match. #### POST `/api/users/company/:id` — Auth (role 3) Create a user under a company. `role_id` is optional and defaults to 4 (User). Only Company Admin (3) and User (4) are allowed on public interfaces. **Request:** ```json { "email": "string", "password": "string", "first_name": "string", "last_name": "string", "tel_1": "string | null", "tel_2": "string | null", "company_address_id": "string | null", "notes": "string | null", "active": true | false, "role_id": 3 | 4, "managed_property_ids": ["ObjectID", "..."] } ``` #### GET `/api/users/:id` — Auth Get a single user. #### PUT `/api/users/:id` — Auth Partial update. Supported fields: `email`, `first_name`, `last_name`, `tel_1`, `tel_2`, `company_id`, `company_address_id`, `active`, `notes`, `last_login`, `role_id`, `language`, `ps_theme`, `managed_properties`. Company admins (role 3) may change another user's `role_id` but only to Company Admin (3) or User (4), and never their own role. #### POST `/api/users/:id/change-password` — Auth Change own password. **Request:** ```json { "current_password": "string", "new_password": "string" } ``` #### PUT `/api/users/:id/reset-password` — Auth (role 3) Admin resets a user's password. **Request:** ```json { "password": "string" } ``` #### DELETE `/api/users/:id` — Auth Soft-delete (sets `active: false`). #### POST `/api/users/:id/managed-properties` — Auth Add a managed property to a user. #### PUT `/api/users/:id/managed-properties/:propertyId` — Auth Update a specific managed property assignment. #### DELETE `/api/users/:id/managed-properties/:propertyId` — Auth Remove a managed property from a user. --- ### Logs #### GET `/api/logs` — Auth (role 1-3) Paginated audit log. Role 1 sees all logs; roles 2-3 see only their company's logs. **Query params:** - `?page=` — page number (default 1, 50 per page) **Response:** ```json { "logs": [ /* Logger[] */ ], "page": 1, "totalPages": 2, "total": 100 } ``` --- ### File Download #### GET `/api/files/:type/:filename` — Auth Download a stored file. Path traversal is blocked. --- ### Bug Reports User-submitted bug reports. **Access rules:** Company Admins (3) see/update their own company's reports; regular users (4) see only their own submissions and cannot update or view the report. #### POST `/api/bug-reports` — Auth Create a bug report. `company_id` and `user_id` are taken from the JWT. `status` defaults to `open`, `urgency` to `medium`. **Request:** ```json { "title": "Button does nothing", "description": "The save button on the invoices page has no effect.", "category": "bug", "urgency": "high", "page": "/invoices" } ``` #### GET `/api/bug-reports` — Auth (scoped) List bug reports. Role 3: own company; role 4: own submissions. **Query params:** - `?status=` - `?urgency=` - `?company_id=` — Company Admin only - `?q=` — search title/description Returns `[]` when empty. #### GET `/api/bug-reports/report` — Company Admin only Aggregate stats: `total`, `by_status`, `total_open`, `open_by_urgency`, `open_by_age` (buckets `0-7`, `8-30`, `31-90`, `90+` days). #### PUT `/api/bug-reports/:id` — Company Admin (own company) Update `status`, `urgency`, or `category`. Regular users get `403`. **Request:** ```json { "status": "resolved", "urgency": "low" } ``` #### POST `/api/bug-reports/:id/comments` — Auth (scoped) Append a comment to a bug report (dev ↔ reporter thread). Access matches the list scope: Company Admin own company, regular users only their own submissions. The server resolves `author_name` from the JWT. Returns `400 "Bug is closed"` when the bug's `status` is `closed` — replies are only allowed before closing. Comments are embedded in the bug report and are included in `GET /api/bug-reports` responses. **Request:** ```json { "text": "Reproduced, fixing now." } ``` **Response (201):** ```json { "user_id": "6a...", "author_name": "System Admin", "text": "Reproduced, fixing now.", "create_date": "2026-08-01T16:43:38Z" } ``` ### Public Pages Public listing endpoints — no auth. Only properties with `list-publicly` enabled and a `for-rent`/`for-sale` status are returned, and the payload is privacy-stripped (no street address, GPS, national ID, documents, or internal fields). Properties marked **Sold** or **Rented** are automatically de-listed (`list-publicly` is forced to `false`). #### GET `/api/public/check?slug=` — Public Checks whether a public URL name is available. Returns `{"available": true|false, "slug": "..."}`. #### GET `/api/public/companies/:slug` — Public Returns a company's public showcase page. `:slug` is the company's `public-slug`. Response: `{"company": {...}, "properties": [...]}`. **`company`** (public-safe): `id`, `name`, `slug`, `logo`, `city`, `website`. **`properties[]`** (public-safe): `id`, `name`, `status`, `status_slug` (slugified status, e.g. `for-rent`, for localized labels), `structure_type`, `city`, `country`, `country_code`, `bedrooms`, `bathrooms`, `square_meters`, `floor_number`, `parking_spaces`, `elevator_count`, `rental_price`, `sale_price`, `currency`, `photo` (base64 data URL), `company_id`, `company_slug`, `company_name`. #### GET `/api/public/countries` — Public Returns the countries that have at least one public listing (only properties from companies with a public page enabled count), for the country switcher and the countries index page (`/public/countries`). Response: `{"countries": [{"code": "de", "name": "Germany", "count": 11}]}`, sorted by name. #### GET `/api/public/countries/:code` — Public Returns public listings across all opted-in companies for a country. `:code` is a lowercase ISO 3166-1 alpha-2 code (e.g. `al`). Response: `{"country_code": "al", "country_name": "Albania", "properties": [...]}` — each property matches the public-safe shape above. Returns `400` for a non-ISO-format code and `404` for an unknown ISO code (e.g. `xx`). #### GET `/api/public/properties/:id` — Public Returns a single public property's full detail for the property detail page (`/public/property/:id`). `:id` is the property ObjectID. Returns `404` for a property that is not publicly listed (`list-publicly` false), does not have a `for-rent`/`for-sale` status, or belongs to a company without a public page. Response `{"property": {...}}` — the public-safe shape above plus: - `state`, `postal_code` — address state/postal code (still no street address or GPS) - `description` — property description - `photos` — array of base64 data URLs for **every** photo (carousel); falls back to `[photo]` if none are storable #### POST `/api/public/lead` — Public Creates a lead from the public contact form. Body: ```json { "company_slug": "bavaria-immobilien", "property_id": "ObjectID (optional)", "name": "string", "email": "string", "phone": "string (optional)", "message": "string (optional)", "consent": true, "website": "" } ``` `consent` must be `true`; `website` is a honeypot (must be empty). Creates a `Prospect` with stage `new`, source `public-website`, next-follow-up set to now, linked to `property_id` when it belongs to the company and is public. Per-IP rate limited (5/hour). Returns `201 {"ok": true}`. Note: no audit log entry (no JWT user — leads are tagged via `source`). ### Chat #### POST `/api/chat` — Auth Streaming chat completion backed by Ollama. Returns an SSE stream used by `useChat()` from `@ai-sdk/vue`. Limited to **100 requests per user per day** (UTC date). Exceeding the limit returns `429` with `{"error":"Daily chat limit of 100 requests reached"}`. Counted per request to `/api/chat` and `/api/ws/chat`, stored in the `chat_usage` collection. When `LLM_PROVIDER=deepseek`, the paid AI assistant is additionally capped by a **per-company monthly AI budget** (default `$3.50`, configurable via `AI_MONTHLY_BUDGET_USD`). Usage is metered at worst-case deepseek rates (cache-miss input `$0.14/1M`, output `$0.28/1M`) and rolled up into the `chat_ai_usage` collection keyed by company and month. Once the budget is reached, `/api/chat` and `/api/ws/chat` reply with a message saying the monthly usage limit is reached (an exact FAQ or cached match is still served) until the 1st of the next month. Cached answers, tool calls against local data, and free providers like Ollama do not count against the budget. **Request:** ```json { "id": "string", "messages": [ { "id": "string", "role": "user" | "assistant", "parts": [ { "type": "text", "text": "string" } ] } ] } ``` **Response:** SSE (`text/event-stream`) with UIMessageChunk events: ``` data: {"type":"text-start","id":""} data: {"type":"text-delta","id":"","delta":"Hello"} data: {"type":"text-delta","id":"","delta":"! How"} data: {"type":"text-end","id":""} data: {"type":"data","id":"","payload":{"type":"properties","items":[...]}} ``` The `data` event with `type: "properties"` is emitted when the chat tool returns property card data, enabling the UI to render a carousel of matching properties below the response. #### GET `/api/ws/chat?token=` — Public (token via query) WebSocket alternative to the SSE chat endpoint. Uses a JWT token passed as a query parameter instead of the `Authorization` header (necessary because WebSocket upgrade cannot set custom headers in all clients). Behavior mirrors `POST /api/chat` with the same request/response message format, sent over WebSocket frames as JSON. Shares the same **100 requests per user per day** limit. **WebSocket message types:** | `type` | Direction | Description | |--------|-----------|-------------| | `text-start` | Server → Client | Assistant message begins, contains `id` | | `text-delta` | Server → Client | Text chunk, contains `id` and `delta` | | `text-end` | Server → Client | Assistant message ends | | `data` | Server → Client | Structured data payload (e.g. property cards) | | `error` | Server → Client | Error message | **Implementation details:** - Default provider is a native Ollama client (`http://192.168.1.96:11434/api`, model `qwen3:4b`, override via the `OLLAMA_MODEL` env var) using the `/api/chat` endpoint with `"think": true` so the model streams real content (reasoning lands in a separate `thinking` field that the client skips). Set `LLM_PROVIDER=deepseek` with `DEEPSEEK_API_KEY` to route through `github.com/sashabaranov/go-openai` to the paid deepseek endpoint instead. Set `LLM_PROVIDER=openrouter` with `OPENROUTER_API_KEY` to route through OpenRouter pinned to the free model `google/gemma-4-26b-a4b-it:free` (no per-token cost). - The Ollama OpenAI-compatible `/v1` endpoint ignores the `think` option entirely, so qwen models stream empty `content` plus a reasoning chain; the native `/api/chat` + `think:true` path is used when `LLM_PROVIDER` is unset or `ollama`. Note `think:false` leaks the reasoning chain into `content` on qwen3:4b, which is why the client uses `think:true` and drops the `thinking` field. - Supports **54 tools** (48 `get_*` read tools, a `get_documentation` help tool, and 6 `create_*` write tools) the AI can call to answer property-management questions and create records - The system prompt instructs the AI to always call a database tool before answering; it never makes up numbers - **Rule 7** ("I cannot answer that"): If no database tool exists to answer the query, the assistant must say "I cannot answer that" — it must NOT compute averages, totals, comparisons, or estimates from tool results - Tool call results are injected as `tool` role messages into the conversation context - After up to 5 tool-call rounds, a final natural-language answer is streamed as text-start/text-delta/text-end events - Empty message list falls back to sending `"Hello"` as a user message - Includes Hermes-family workarounds: `sanitizeArgs` replaces schema-objects in arguments with zero values; `normalizeToolArgs` translates alternate parameter names; `extractToolCallFromText` parses tool calls embedded in text output - **Entity resolution**: `ExtractPotentialNames` first tries regex for capitalized names; if no capitalized names found, falls back to individual non-stop-word tokens `>=3` chars — enabling lookups on lowercase input like "james smith" - Messages are persisted to the `conversations` collection after each chat interaction (upserts by `chat-id`) **Available chat tools (54 total):** | Tool | Description | |------|-------------| | `get_properties_by_status` | List properties by status name (e.g. "For Rent", "Vacant") | | `get_property_summary` | All properties grouped by status | | `get_properties_by_structure` | Properties grouped by structure type | | `get_properties_by_status_and_structure` | Filter by BOTH status and structure | | `get_properties_by_details` | Filter by amenities (parking, bedrooms, elevator, city, etc.) | | `get_ticket_summary` | Open tickets grouped by severity | | `get_tickets_by_severity` | List tickets by severity (Critical, High, etc.) | | `get_tickets_by_status` | List tickets by status name and optionally severity name | | `get_invoice_summary` | Overdue and pending invoice counts | | `get_overdue_invoices` | List all overdue invoices with tenant + property details | | `get_lease_summary` | Active lease count and upcoming endings | | `get_leases_ending_soon` | List leases ending within N days with full details | | `get_vendor_summary` | Active vendors grouped by type | | `get_vendors_by_type` | List vendors by vendor type | | `get_expense_summary` | Expenses grouped by category | | `get_expenses_by_property` | Expenses grouped by property, sorted by total descending | | `get_expenses_by_status` | Expenses filtered by status name (e.g. "Paid", "Pending"), grouped by property | | `get_property_with_most_expenses` | Single property with the highest total expenses | | `get_running_expense_summary` | Running expenses grouped by status | | `get_running_expenses_by_property` | Recurring expenses grouped by property, sorted by total descending | | `get_running_expenses_by_status` | Recurring expenses filtered by status name, grouped by property | | `get_property_with_most_running_expenses` | Single property with the highest total recurring expenses | | `get_tenant_summary` | Tenant counts and names | | `get_payment_summary` | Payments grouped by method | | `get_revenue_summary` | Collected payments vs outstanding invoices snapshot | | `get_properties_with_open_tickets` | Properties with open tickets, grouped with counts | | `get_property_by_name` | Full details for a single property by name | | `get_tenants_at_property` | Tenants at a specific property | | `get_property_expenses` | Expenses for a specific property | | `get_tickets_by_property_name` | Tickets for a specific property | | `get_invoices_by_tenant` | Invoices for a tenant by name | | `get_portfolio_summary` | High-level portfolio overview | | `get_tenant_contact` | Phone and email for a tenant by name | | `get_vendor_contact` | Contact details for a vendor (company/contractor) by name — NOT for tenants | | `get_lease_by_tenant` | Find active lease for a tenant by name | | `get_tickets_by_tenant` | Find maintenance tickets for properties leased by a tenant name | | `get_prospect_summary` | Prospects grouped by stage (new/contacted/viewing/offer/closed-won/closed-lost) | | `get_prospects_by_stage` | List prospects in a given pipeline stage | | `get_prospects_by_property` | List prospects for a specific property | | `get_next_prospect_follow_up` | Prospects sorted by earliest next follow-up date (who to follow up next) | | `get_prospect_contact` | Phone, email, and stage for a prospect by name | | `get_public_page_url` | Company's public showcase page URL and how many properties are listed on it | **Write tools (create records):** | Tool | Description | |------|-------------| | `create_prospect` | Create a prospect (name required; property/stage/email/phone optional) | | `create_vendor` | Create a vendor (company name required; vendor type/property optional) | | `create_ticket` | Create a maintenance ticket for a property (description + property required) | | `create_running_expense` | Create a running/recurring expense for a property and vendor | | `create_property` | Create a property from minimal fields (name + address); the user is asked to finish the full details in the app | | `create_tenant` | Create a tenant (name + phone/email) | **Write confirmation flow:** Every `create_*` tool is **write-protected**. The first time the AI calls it, the server returns a confirmation prompt instead of writing anything. The assistant must ask the user for confirmation and only call the same tool again with `confirm: true` to actually create the record. This holds even if the model sets `confirm: true` on the first call — a prior tool round-trip for the same tool is required before any write. Create requests never hit or fill the response cache. Permission checks mirror the REST controllers (role 3 Company Admin can create anywhere; role 4 users must manage the property — and properties respect the free-plan 5-property quota, tenants are un-gated). All creates are audit-logged via `logScopeAction`. --- ### Help #### GET `/api/help/:topic` — Auth Returns the help content for the given topic (`properties`, `tickets`, `expenses`, `leases`, `tenants`, `vendors`, `payments`, `invoices`, `expense-templates`, `chat-assistant`, `general`, etc.) as an HTML page. Markdown files are read from `./docs/help/:topic.md` and converted to styled HTML on the server. **Query params:** - `?theme=light|dark` — adjusts background and text colors (defaults to `light`) --- ### Pricing & Plans The plan catalog (`plans` collection) is the pricing contract the payment processor consumes. Prices are minor units (cents/pence). Fixed GBP/USD overrides win; live exchange rates are only the fallback for currencies without an override. #### GET `/api/plans?currency=GBP` — Public (no auth) Active plan catalog with prices resolved for the requested currency (`?currency=XXX`, default `EUR`): ```json [ { "id": "...", "slug": "free", "name": "Free", "price_eur": 0, "prices": {}, "price_minor": 0, "currency": "GBP", "limits": { "properties": 5, "storage_bytes": 524288000 }, "active": true, "sort_order": 1 }, { "id": "...", "slug": "pro", "name": "Pro", "price_eur": 1500, "prices": { "GBP": 1300, "USD": 1600 }, "price_minor": 1300, "currency": "GBP", "limits": { "properties": 0, "storage_bytes": 0 }, "active": true, "sort_order": 2 } ] ``` #### Admin CRUD — role 3 `POST /api/plans`, `PUT /api/plans/:id`, `DELETE /api/plans/:id`. `DELETE` is a soft delete (`active: false`) — deactivated plans disappear from the catalog but the document stays for history. Validate `slug` + `price-eur` (int64 minor units) on create. --- ### Lookup Tables All lookup tables follow the same CRUD pattern. Create/Update/Delete are admin-only. Get/GetAll are available to any authenticated user. **Read routes (auth):** | Method | Path | Collection | |--------|------|------------| | GET | `/api/structure-types` | `structure_types` | | GET | `/api/structure-types/:id` | — | | GET | `/api/property-statuses` | `property_statuses` | | GET | `/api/property-statuses/:id` | — | | GET | `/api/currencies` | `currencies` | | GET | `/api/currencies/:id` | — | | GET | `/api/purchase-statuses` | `purchase_statuses` | | GET | `/api/purchase-statuses/:id` | — | | GET | `/api/expense-categories` | `expense_categories` | | GET | `/api/expense-categories/:id` | — | | GET | `/api/expense-statuses` | `expense_statuses` | | GET | `/api/expense-statuses/:id` | — | | GET | `/api/invoice-statuses` | `invoice_statuses` | | GET | `/api/invoice-statuses/:id` | — | | GET | `/api/payment-methods` | `payment_methods` | | GET | `/api/payment-methods/:id` | — | | GET | `/api/vendor-types` | `vendor_types` | | GET | `/api/vendor-types/:id` | — | | GET | `/api/lead-sources` | `lead_sources` | | GET | `/api/lead-sources/:id` | — | | GET | `/api/severities` | `severities` | | GET | `/api/severities/:id` | — | | GET | `/api/ticket-statuses` | `ticket_statuses` | | GET | `/api/ticket-statuses/:id` | — | | GET | `/api/roles` | `roles` | | GET | `/api/roles/:id` | — | **Admin write routes (role 3):** | Method | Path | |--------|------| | POST/PUT/DELETE | `/api/structure-types[/:id]` | | POST/PUT/DELETE | `/api/property-statuses[/:id]` | | POST/PUT/DELETE | `/api/currencies[/:id]` | | POST/PUT/DELETE | `/api/plans[/:id]` | | POST/PUT/DELETE | `/api/purchase-statuses[/:id]` | | POST/PUT/DELETE | `/api/expense-categories[/:id]` | | POST/PUT/DELETE | `/api/expense-statuses[/:id]` | | POST/PUT/DELETE | `/api/invoice-statuses[/:id]` | | POST/PUT/DELETE | `/api/payment-methods[/:id]` | | POST/PUT/DELETE | `/api/vendor-types[/:id]` | | POST/PUT/DELETE | `/api/lead-sources[/:id]` | | POST/PUT/DELETE | `/api/severities[/:id]` | | POST/PUT/DELETE | `/api/ticket-statuses[/:id]` | | POST/PUT/DELETE | `/api/roles[/:id]` | Per-company limits apply when creating lookup records: | Collection | Limit | Error when exceeded | |------------|-------|---------------------| | `structure_types` | 32 active / 64 total records per company | `400` `{"error": "Limit of 32 active reached for this company"}` / `400` `{"error": "Limit of 64 total reached for this company"}` | | `vendor_types` | 64 active / 128 total records per company | `400` `{"error": "Limit of 64 active reached for this company"}` / `400` `{"error": "Limit of 128 total reached for this company"}` | | `lead_sources` | 64 active / 128 total records per company | `400` `{"error": "Limit of 64 active reached for this company"}` / `400` `{"error": "Limit of 128 total reached for this company"}` | Soft-deactivated (inactive) records do not count toward the limit. Each lookup record has the shape: ```json { "id": "ObjectID", "name": "string", "slug": "string", "description": "string | null", "active": true, "create_date": "ISO8601" // Some also have: last_updated, last_updated_by, company_id } ``` The `slug` field is a URL-friendly identifier (e.g. `"for-rent"`, `"paid"`, `"open"`) that can be used with the `?status=` query parameter on list endpoints. --- ## File Upload Pattern All file uploads follow the same convention: 1. Send files as **base64 data URLs** in JSON (e.g. `"link": "data:image/jpeg;base64,/9j/4AAQ..."`) 2. The server strips the `data:...;base64,` prefix 3. Files are saved to the filesystem via `utils.SaveBase64File()` 4. The database stores only the server-side path (e.g. `./files/receipts/`) 5. On GET endpoints, files are read back from disk and returned as base64 data URLs `SaveBase64File` rejects files whose decoded size exceeds **10 MB** (`utils.MaxUploadBytes`). It also normalizes uploads: images (JPEG/PNG/GIF) are resized to at most 1280px on the longest side and re-encoded as quality-85 JPEG — or kept as PNG when the image has transparency; already-compressed formats (PDF, DOCX/XLSX, ZIP, WebP, ...) are stored as-is; everything else is gzip-compressed. Gzip-stored files are transparently decompressed when served (`utils.ReadStoredFile`). Image mime types on GET responses are taken from the actual stored bytes (`utils.DataURLMime`), so a PNG that was normalized to JPEG is served as `image/jpeg` even if the original file was named `.png`. For property photos, use **multipart form upload** at `POST /api/properties/:id/photos` instead. That endpoint applies the same 10 MB cap and resizes photos server-side to ~500 KB. --- ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `MONGODB_URI` | — | MongoDB connection string | | `MONGODB_DATABASE` | `property_portal` | Database name | | `PORT` | `8080` | HTTP server port | | `JWT_SECRET` | `property-portal-secret-change-in-production` | JWT signing key | ### Ollama (Chat) The chat endpoint connects to a local Ollama instance. Configured in `api/controllers/chat_controller.go`: | Setting | Value | |---------|-------| | Base URL | `http://192.168.1.96:11434/api` (native `/api/chat`) | | Model | `qwen3:4b` (override via `OLLAMA_MODEL`) | --- ### Invoice-Gen (Scheduled Auto-Billing) A standalone Go service (`invoice-gen/`) runs nightly as a Docker container (`invoice-gen` in `docker-compose.yml`) that generates rent invoices for active auto-billing leases. **How it works:** 1. Connects to the same MongoDB database 2. Queries leases where `active: true`, `auto-billing: true`, and `start-date ≤ now ≤ end-date` 3. For each lease, calculates completed billing periods based on its `period` (14, 30, 45, 60, or 90 days) 4. Creates a "Pending" invoice for the total rent amount per period, deduplicating by `period-month` (YYYY-MM) 5. Invoice due-date is set to period-end + 15 days **Idempotency:** A second run produces zero new invoices — the service checks for existing invoices by `lease + period-month` before creating. **Test scenarios** (see `invoice-gen/TestPlan.md`): | Period | Start | Completed Slots | Expected Invoices | |--------|-------|-----------------|-------------------| | 14d | -45 days | 3 | 2 (dedup by month) | | 30d | -100 days | 3 | 3 | | 45d | -140 days | 3 | 3 | | 60d | -190 days | 3 | 3 | | 90d | -280 days | 3 | 3 | --- ## Error Responses All endpoints return error responses in the format: ```json { "error": "error message string" } ``` Error messages are localized: send an `Accept-Language` header (e.g. `de`, `es`, `fr`, `hr`, `sq`) and the most common messages (invalid IDs, access denials, the "not found" family, login/auth errors) are returned in that language; any message without a translation falls back to English. The web UI sends this header automatically from the user's chosen language. HTTP status codes: - `400` — Bad request (validation, missing fields) - `401` — Unauthorized (missing/invalid JWT) - `403` — Forbidden (insufficient role) - `404` — Not found - `500` — Internal server error --- ## Development Policy: New Features & Fields Any new feature or model field added to this API must expose searchable query parameters on the corresponding list endpoint. The UI (`./ui`) must then be updated to surface the new fields and their filters on the relevant pages. Additionally, `API-Guide.md`, `FAQ.md`, and `UserExperience.md` must be kept in sync with every change. See `AGENTS.md` for the full policy. --- ## Database Indexes Indexes are automatically ensured on startup for these collection/field pairs: - `expenses` → `property-id` - `invoices` → `lease` - `payments` → `invoice` - `properties` → `company-id` - `tickets` → `property-id` - `expenses` → `property-id` - `expense_templates` → `property-id` # Auto-Billing How does auto-billing work? A service runs nightly to generate rent invoices. It finds all active leases with auto-billing enabled, calculates which billing periods have completed, and creates a Pending invoice for each one. Is it safe to run auto-billing multiple times? Yes. It checks for existing invoices before creating new ones, so duplicates are never created. # Chat Assistant How does the chat assistant work? The chat runs on a locally hosted AI server. It has access to your property data through a set of tools and always looks up information before answering, so it never makes up numbers. You can ask it questions about properties, tickets, leases, invoices, payments, expenses, vendors, and tenants. If it cannot find an answer, it says I cannot answer that. What kind of questions can I ask? For properties you can ask how many are for rent, show vacant apartments, or which have 3 bedrooms with parking. For tickets you can ask how many are open or show critical ones. For leases you can ask how many are active or which end this month. For invoices you can ask how many are overdue or list overdue ones with tenant names. For payments you can ask what was collected or which methods were used. For expenses you can ask totals by category or which property had the most. For vendors you can ask how many are active or list by type. For tenants you can ask for counts, names, or contact info. Can the chat answer how-to questions too? Yes. The assistant can also answer questions about how the system works, like what is an invoice, how do file uploads work, or what are the user roles. Common how-to questions are answered instantly from a cached knowledge base — even when you rephrase them, the assistant recognises the meaning and replies without a slower model call. Does the chat understand lowercase names? Yes. If you type james smith instead of James Smith, it will still find the right person. Can the chat create records for me? Yes. It can create prospects, vendors, tickets, expenses, properties, and tenants. Before it creates anything it asks you to confirm, and nothing is written until you agree. For example, try "create a ticket for property X: leaking tap, due Friday" or "add a vendor called Green Energy for the office building". Creating a property needs only a name and address from chat; you finish the remaining details in the app. The same permission rules as the app apply. Does each user have their own chat history? Yes. Each user has one persistent conversation. The full history loads every time you open the chat page. # Compliance — Albania What certificates does a property in Albania need? A property in Albania should have its title deed / ownership registration recorded and a valid energy performance certificate. What is the title deed registration? Property ownership is recorded in the title deed registry. Registration is permanent but must be updated whenever the title changes. What is the energy performance certificate? Energy performance certificates are required when selling or renting a property. Re-issue after renovation. Does this cover short-term rentals? No. Short-term rental licensing (via e-Albania) applies only to short lets and is outside this product's scope. This guidance covers long-term residential letting. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (ownership-registration, energy-passport), reference number, and expiry date. Certificates that are expiring within 60 days are flagged as expiring. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Czechia What certificates does a property in Czechia need? A property in Czechia should have its cadastre (Land Registry) entry recorded, a valid energy performance certificate (PENB), and a rental registration for rented units. What is the Land Registry (Katastr nemovitosti)? The Czech Land Registry records property ownership. The entry is permanent but must be updated whenever the title changes. Any sale or transfer relies on the current record. What is the PENB certificate? The PENB (Prukaz energeticke narocnosti budovy) is the Czech energy performance certificate. It is required for sale, rental, or significant construction changes and is valid for 10 years. What is rental registration? Rented properties should be registered for each lease term. Re-register when a new lease starts. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (land-registry, energy-passport, rental-registry), reference number, and expiry date. Certificates that are expiring within 90 days are flagged as expiring. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Georgia What certificates does a property in Georgia need? A property in Georgia should have its ownership registration with the National Agency of Public Registry (NAPR) recorded, a valid energy performance certificate, and documented fire safety compliance. What is the NAPR registration? The National Agency of Public Registry (NAPR) is the Georgian authority that records property ownership. Registration is permanent but must be updated whenever the title changes. Any sale or transfer relies on the current NAPR record. What is the energy performance certificate? Georgia requires an energy performance certificate when selling or renting a property. Re-issue it after renovation, because the energy rating can change. What is fire safety compliance? Fire safety compliance documents that the building meets fire-safety requirements. Re-verify it when the use or occupancy of the building changes. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (landlord-register, ownership-registration, energy-passport, fire-safety), reference number, and expiry date. Certificates that are expiring within 60 days are flagged as expiring. What is the Landlord Register? The Compulsory Landlord Register (მემამულეთა რეესტრი) is the Georgian register of landlords letting residential property. Registration is mandatory — failing to register carries a GEL 100 penalty. What is the 5% vs 20% rent tax? Registered landlords pay a 5% flat tax on rental income. Unregistered landlords are taxed at 20%. Registering with the Landlord Register is therefore worth many times the GEL 100 penalty — the tax saving dwarfs the fee. How do I record this in Property Portal? Open the property's Compliance section and add a certificate of type landlord-register with the registration reference number. It is tracked like any other certificate (valid / expiring / expired). Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Kosovo What certificates does a property in Kosovo need? A property in Kosovo should have its ownership registration recorded and a valid energy performance certificate. What is the ownership registration? Property ownership is registered in the cadastre. Registration is permanent but must be updated whenever the title changes. What is the energy performance certificate? Energy performance certificates are required when selling or renting a property. Re-issue after renovation. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (ownership-registration, energy-passport), reference number, and expiry date. Certificates that are expiring within 60 days are flagged as expiring. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Moldova What certificates does a property in Moldova need? A property in Moldova should have its ownership registration with the Public Services Agency recorded, a valid energy performance certificate, and each lease contract registered with the tax service. What is the ownership registration? Ownership is registered with the Public Services Agency. Registration is permanent but must be updated whenever the title changes. What is the energy performance certificate? Energy performance certificates are mandatory in Moldova (enforced since 27 April 2025). Re-issue after renovation, because the energy rating can change. What is lease registration? Rental contracts must be registered with the tax service — register each lease. Registration supports the formalisation drive and keeps rental income compliant. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (ownership-registration, energy-passport, lease-registration), reference number, and expiry date. Certificates that are expiring within 60 days are flagged as expiring. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Serbia What certificates does a property in Serbia need? A property in Serbia should have its cadastre (Katastar) registration recorded, a valid energy passport (energetski pasos), and property tax registration. What is the Katastar registration? The Katastar nepokretnosti is the Serbian real estate cadastre that records property ownership. Registration is permanent but must be updated whenever the title changes. Any sale or transfer relies on the current record. What is the energetski pasos? The energetski pasos is the Serbian energy performance certificate. It is required when selling or renting a building, or after a major renovation, because the energy rating can change. What is property tax registration? Property tax registration documents that the property is registered with the tax authority. Re-verify it annually. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (ownership-registration, energy-passport, tax-registration), reference number, and expiry date. Certificates that are expiring within 60 days are flagged as expiring. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance — Turkey What certificates does a property in Turkey need? A property in Turkey should have its title deed registration (Tapu) recorded, a valid energy performance certificate (Enerji Kimlik Belgesi, EKB), and — for newer buildings in seismic zones — an earthquake risk assessment. What is the Tapu? The Tapu is the Turkish title deed proving ownership and is registered in the Land Registry. It is permanent but must be re-verified whenever ownership changes. Any sale or transfer requires the current Tapu. What is the Enerji Kimlik Belgesi? The Enerji Kimlik Belgesi (EKB) is the Turkish energy performance certificate. It is required when selling or renting a property and is valid for 10 years. It must be re-issued after a major renovation because the energy rating can change. What is the earthquake risk assessment? For buildings in seismic zones, an earthquake risk assessment documents the building's structural resilience. It should be re-assessed after any structural work or a major seismic event. How do I record these in Property Portal? Open the property's Compliance section and add each certificate with its type (tapu, energy-passport, earthquake-risk), reference number, and expiry date. Certificates that are expiring within 90 days are flagged as expiring. What about KVKK (personal data protection)? Turkey's Law on the Protection of Personal Data (KVKK) governs how tenant and landlord personal data is handled. Store tenant documents (leases, identity documents, contact details) in the property's private Documents section — they never appear on the public Marketplace — and collect only what the lease requires. You remain responsible for how you collect and use tenant data. Is this legal advice? No. This is informational guidance and does not constitute legal advice. Verify current requirements with a local professional. # Compliance What is compliance tracking in Property Portal? Compliance tracking lets you record the regulatory certificates and registrations each property needs — for example energy performance certificates, title-deed registrations, or rental-registry entries. Each certificate stores its type, reference number, issue and expiry dates, and an optional uploaded document. The system flags certificates that are expiring soon or already expired so nothing lapses unnoticed. Where do I see compliance data? Each property page has a Compliance section where certificates are listed with their status (valid, expiring, or expired). The dashboard shows a compliance widget that summarizes, across your portfolio, how many certificates are valid, expiring, or expired, plus which required certificates are missing for each property. How do I add a certificate to a property? Compliance tracking is a per-property opt-in — first enable compliance tracking for the property in its settings. Then open the property page, go to the Compliance section, and add a certificate with its type, number, issue date, and expiry date. In the add form, certificates required for the property's location (based on its address country) are suggested at the top as clickable chips — pick one to prefill the type — and a link opens the country compliance guide for details. You can also attach a scanned copy or PDF of the certificate; it is stored as a file, not as text in the database. Certificates can also be recorded through the chat assistant — it will ask for your confirmation before saving anything. What certificate types are tracked? The required certificate types depend on your market, which is derived from your company's country. Supported markets currently include Turkey, Georgia, Czechia, Serbia, Moldova, Albania, and Kosovo. For example, Czechia requires a PENB energy performance certificate for sale or rental, and Serbia requires an energetski pasos. For markets without a dedicated rule set, you can still record any certificate you choose; no "required" list is enforced. Who can see compliance data? The same permission rules as the rest of the app apply: company admins can see all properties in the company, and role-4 users can only see certificates for the properties they manage. Is this legal advice? No. Compliance tracking and guidance are informational and do not constitute legal advice. Regulatory requirements change, so verify them with a local professional before relying on them. Can the AI assistant help with compliance? Yes. The assistant can summarize compliance status, list certificates that are expiring or expired, and record certificates you ask for. On active paid plans it can also give market-specific compliance guidance for your company's country, based on the built-in rule registry. # CSV Export Which pages let me download a CSV? Six pages have a CSV button: Tickets, Expenses, Payments, Invoices, and Leases. The CSV includes the record ID, all visible fields, and a currency column. Attachments and photos are excluded. Does the CSV respect my current filters? Yes. Whatever data is currently displayed, including active search filters, is what gets exported. # Automatic Expenses What is an Automatic Expense? An Automatic Expense is a recurring cost you define **once** on a property — for example, a mortgage payment, an insurance premium, ground rent, a service charge, or electricity. Instead of re-entering the cost every period, you create a template and the system **automatically generates an Expense record for each period** (usually overnight). Your only job each period is to mark the generated entry as Paid and attach the receipt. What fields does an Automatic Expense have? A template stores: the property, a category (from your company's expense categories), the vendor, the amount and currency, the **amount mode** (Fixed or Variable), the frequency (Monthly, Quarterly, or Yearly), the start date, an optional end date, the due day of the period (defaults to the start date's day), a "Paid by Tenant" flag, and notes. You also see the next period and next due date at a glance. What are the two amount modes? - **Fixed** — the amount is constant every period (mortgage, insurance premium). The system writes the template amount into each generated entry exactly. - **Variable** — the amount changes each period (electricity, water). The template stores the *last known* amount as a pre-fill. When the entry is generated, it arrives Pending with that pre-filled amount; you adjust it to the actual bill, attach the receipt, and mark it Paid. The system remembers the final Paid amount and uses it as the next pre-fill. How is an Automatic Expense different from a recurring Expense? An **Expense** is a single cost record — one-off or recurring, created by hand. An **Automatic Expense** is the template that generates those records automatically. Think of it as: Automatic Expense = the standing order; the generated Expense records = each individual payment. You manage templates on the Automatic Expenses page; the generated entries appear on the Expenses page under the **Recurring** tab with an "auto" badge. How do I create an Automatic Expense? Go to the Automatic Expenses page and click "Add Automatic Expense." Choose the property, category, vendor, amount, **amount mode** (Fixed or Variable), currency, frequency (Monthly/Quarterly/Yearly), start date (defaults to today), and optional end date. The system starts generating entries from the start date. Tip: leave the start date at the current month to avoid generating past periods — set it earlier only if you want to backfill history you can mark as Paid. How does the automatic generation work? Every night a background job checks your active templates and creates an Expense record for every period that hasn't been generated yet — with a Pending status, the due date from the template, and (for variable templates) the last-known amount as a pre-fill. You then treat each generated entry like any expense: adjust the amount if needed, mark it Paid when the payment clears, and attach the receipt. What happens with the "Paid by Tenant" option? If a template is marked "Paid by Tenant," each generated entry also creates an invoice for the active tenant on that property. The tenant invoice is created together with the entry, with a due date 15 days out. Can I edit an Automatic Expense? Yes — admins and the creator can edit the template. **Changes apply from the next period onward**: entries that were already generated keep their amounts, so your financial history stays accurate. You can also pause a template (it stops generating) or set an end date. Can I delete an Automatic Expense? Only admin-level users (Company Admin and above) can delete an automatic expense. Deleting a template stops future generation but keeps all already-generated Expense records as history. What are the expense categories for? Categories group your automatic costs (Mortgage, Insurance, Service Charge, Utilities, ...). Categories are a **company setting**: each company manages its own list under Company Admin → Expense Categories. When a company first opens the list, it is seeded with the standard defaults, which you can then rename, add to, or remove. Who can see which automatic expenses? Access is scoped by role: Company Admins see all automatic expenses in their company; Users (property managers) only see automatic expenses for properties they manage. Site administrators see all companies. # Expenses **Two concepts — that's it.** PropertySage has exactly two cost concepts: | Concept | What it is | When to use it | |---|---|---| | **Expense** | Every cost record — one-off or recurring. One page, filterable by *One-off* / *Recurring*. Recurring records show a period, may carry a vendor, and get an "auto" badge when generated from a template. | Any cost associated with a property. | | **Automatic expense** | A set-once template (property, amount, frequency) that **spawns an expense record every period** — you never re-enter it. Two amount modes: **fixed** (constant amount) and **variable** (amount changes each period — e.g. electricity). | Recurring costs you want the system to generate for you. | The old "Running Expense" and "Fixed Expense" terms are gone: a running expense was just a recurring expense, and a fixed expense was an automatic-expense template. **Which one should I use?** - Cost happens once → **Expense** (One-off). - Cost repeats and you want to generate it automatically → **Automatic expense** — set it once, the system creates an entry each period (usually overnight). Generated entries appear on the Expenses page under the **Recurring** tab with an "auto" badge, where you mark them Paid and attach receipts. - Cost repeats but you prefer to enter each period by hand → **Expense** (Recurring toggle) — you create a new record each period yourself. --- What is an Expense? An Expense records a cost associated with a property. This could be a one-off repair bill, a service fee, or a recurring cost (water, electricity, insurance). Each expense is linked to a property, categorized by expense type, can carry a vendor and a period (for recurring entries), and can have receipt files attached as proof of payment. For costs that repeat automatically, use Automatic Expenses. How do I create an expense? Navigate to the Expenses page and click "Add Expense." Select the property, choose a category, enter the amount, pick the currency, set the date, and select a status (typically "Pending" if not yet paid or "Paid" if settled). To record a recurring cost by hand, switch on the "Recurring expense" toggle and optionally add a vendor and a period (e.g. "2026-08"). You can add notes, a reference number, and attach receipt files — PDFs, Word documents, or images. How do receipt attachments work for expenses? When you add a receipt, the file is encoded and sent to the server, which saves it to the filesystem under `./files/receipts/`. Only the file path is stored in the database. When you view the expense later, the receipt is loaded from disk and displayed in the browser. You can download any attached receipt by clicking on it. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Can I edit or delete an expense that has already been paid? No. Once an expense's status is changed to "Paid," it becomes locked. Neither the update nor the delete option is available — both in the UI and enforced by the API. This protects your financial records from accidental changes. How do I edit an expense? Click the pencil icon on any row in the Expenses table. An inline dialog opens allowing you to change the status and manage receipts. Only the user who created the expense or a Company Admin can edit it. How do I delete an expense? Only users with admin-level roles (Company Admin and above) can delete an expense. Deletion is a hard delete — the record is permanently removed, and any attached receipt files are also deleted. You cannot delete an expense whose status is "Paid." What are the tabs on the Expenses page? The page has three tabs: **All** (everything), **One-off** (non-recurring expenses), and **Recurring** (expenses with `recurring: true`). Auto-generated entries (from Automatic Expenses) carry a small "auto" badge and their expand row shows the template that produced them. Who can see which expenses? Access is scoped by role: Company Admins see everything in their company; Users (property managers) only see expenses for properties they manage. Site administrators see all companies. What filters are available on the Expenses page? You can filter by property, category, currency, status, amount range, and expense date range. A search box provides client-side filtering, and the Recent/Historical toggle switches between the last 30 days and all time. Can I export expenses to CSV? Yes. Click the "CSV" button on the Expenses page to download all currently displayed expenses as a CSV file. The export respects any active filters you have applied. # File Uploads How do file uploads work? When you attach a file like a receipt or lease document through the web UI, the file is sent to the server, saved to disk, and the path is stored in the database. When you view the record later, the file is loaded and displayed again. Property photos are uploaded through the property edit page. The server resizes them automatically. What is the difference between soft-delete and hard-delete? Soft delete marks the record as inactive but keeps it in the database. This is used for Tenants, Leases, Vendors, and Companies. Hard delete permanently removes the record. This is used for Properties, Expenses, Invoices, and Payments. # General What is Property Portal? It's system to help manage properties. It will allow property managers or property owners to track expenses, rental status, tenants and tickets related to property issues. Who is Property Portal for? It's for people who manage properties like property agents, property managers and property owners. # Help System — UX Design Specification ## Overview This document specifies the design for adding an in-app help system to Property Portal. The system has two surfaces: 1. **Help Drawer** — a right-side slide-out panel accessible from anywhere in the app, showing topic-based help content 2. **Footer FAQ** — an expandable accordion in the footer showing the full FAQ Both surfaces pull content from the canonical markdown files in `./docs/help/`, which are compiled into the i18n locale JSON files at build time via a Makefile pipeline. --- ## Design Principles | Principle | How It Shapes the Design | |-----------|--------------------------| | **Stay in flow** | Help opens as an overlay drawer, never a page navigation. The user's work context is preserved behind the drawer. | | **One source of truth** | The markdown files in `./docs/help/` are the canonical content. They are compiled, translated, and embedded into the app at build time — not fetched at runtime. | | **i18n-first** | Every word in the help system flows through the existing `vue-i18n` pipeline. All six locales (en, de, es, fr, sq, hr) are supported. | | **Progressive disclosure** | Topics are listed hierarchically. Users see a clean list of help topics, expand one, and get the content they need — nothing more. | | **Context-aware** | The Help Drawer can optionally highlight the topic most relevant to the current page (e.g., if you're on the Expenses page, "Expenses" is pre-selected in the help drawer). | --- ## Surface 1: Help Drawer ### Trigger A help icon button (`mdi-help-circle`) is placed in two locations: - **Header (AppHeader.vue)** — visible to all users, both logged-in and unauthenticated. Sits beside the theme toggle and locale switcher. - **Sidebar (ControlPanel.vue)** — visible to authenticated users only. Placed as the last item in the main nav list, after Tenants. Uses `mdi-help-circle` with a muted grey-blue color to visually separate it from the data navigation items. ### Behaviour | Action | Result | |--------|--------| | Click help icon | Right-side `v-navigation-drawer` slides open, 380px wide, with `temporary` mode | | Click outside drawer | Drawer closes | | Press `Escape` | Drawer closes | | Select a topic | Content area shows the selected topic's help text | | Click help icon again | Drawer toggles closed | ### Layout (top to bottom) ``` ┌──────────────────────────────────┐ │ ❓ Help & FAQ ✕ Close │ ← Header with close button ├──────────────────────────────────┤ │ 🔍 Search help... │ ← Client-side filter input ├──────────────────────────────────┤ │ ┌─ General ──────────────────┐ │ │ │ ○ General │ │ │ │ ○ User Roles │ │ ← Scrollable topic list │ │ ○ Login & Passwords │ │ Active topic highlighted │ │ ○ File Uploads │ │ │ └────────────────────────────┘ │ │ ┌─ Features ────────────────┐ │ │ │ ○ Properties │ │ │ │ ○ Tickets │ │ │ │ ○ Expenses │ │ │ │ ○ Running Expenses │ │ │ │ ○ Vendors │ │ │ │ ○ Leases │ │ │ │ ○ Tenants │ │ │ │ ○ Payments │ │ │ │ ○ Invoices │ │ │ └────────────────────────────┘ │ │ ┌─ System ──────────────────┐ │ │ │ ○ Chat Assistant │ │ │ │ ○ Auto-Billing │ │ │ │ ○ Search & Filters │ │ │ │ ○ CSV Export │ │ │ └────────────────────────────┘ │ ├──────────────────────────────────┤ │ (Topic content area) │ ← Renders selected topic │ │ Content is HTML rendered │ Q: What is a Property? │ from locale via v-html │ A: A Property is the core... │ │ │ │ Q: How do I create a new │ │ property? │ │ A: Navigate to the... │ │ │ ├──────────────────────────────────┤ │ 📖 View full FAQ → footer │ ← Link to footer section └──────────────────────────────────┘ ``` ### Component: HelpDrawer.vue **Path:** `ui/src/components/HelpDrawer.vue` **Props:** - `modelValue` (Boolean) — v-model for drawer open/close - `initialTopic` (String, optional) — slug of the topic to pre-select **Emits:** - `update:modelValue` — for v-model binding **Template structure:** ```html
{{ t('help.title') }} mdi-close
{{ group.label }}
{{ topic.label }}
mdi-help-circle-outline

{{ t('help.selectTopic') }}

``` ### State ```js const searchQuery = ref('') const activeTopic = ref(props.initialTopic || null) ``` ### Topic Grouping The 17 help sections are grouped for the sidebar TOC: | Group | Topics | |-------|--------| | General | general, user-roles, login-and-passwords, file-uploads | | Features | properties, tickets, expenses, running-expenses, vendors, leases, tenants, payments, invoices | | System | chat-assistant, auto-billing, search-and-filters, csv-export | ### Context Awareness On mount, if `initialTopic` is not provided, the drawer checks `route.path` to auto-select a topic: | Route contains | Auto-select topic | |----------------|-------------------| | `/property` | properties | | `/ticket` | tickets | | `/expense` | expenses | | `/running-expense` | running-expenses | | `/vendor` | vendors | | `/lease` | leases | | `/tenant` | tenants | | `/payment` | payments | | `/invoice` | invoices | | `/chat` | chat-assistant | | (other) | general | ### Styling - Drawer width: 380px — wide enough for readable content, narrow enough to not dominate the screen - Topic list (left 5 cols): muted background, active topic has a coloured left border accent using `border-left: 3px solid rgb(var(--v-theme-primary))` - Content area (right 7 cols): standard text styling matching the app's `text-body-2` - Q&A pairs: Questions in `font-weight-bold`, answers in regular weight with a 12px left padding to visually indent them - All border-radius: 0 (matching the app's global style) - Dark mode: inherited from Vuetify theme --- ## Surface 2: Footer FAQ ### Trigger The footer already exists at the bottom of every page (except chat). We add an expandable section above the existing copyright line. ### Behaviour - By default, the FAQ section is **collapsed** - A button reads "📖 FAQ" or "❓ Help & FAQ" - Clicking it expands a `v-expansion-panels` accordion showing all 17 FAQ sections - Each section is an expansion panel - Within each panel, Q&A pairs are rendered as definition-style content ### Component: AppFooter.vue (modified) **Updated template:** ```html ``` ### Content Loading The FAQ content is **not** loaded from locale keys directly (that would create 100+ keys per locale). Instead: - A composable `useHelpContent.js` imports the raw markdown content (pre-compiled to HTML at build time) from a single locale key per section - The composable parses the Q&A pairs from the HTML by splitting on headings - This keeps the locale files clean while still supporting i18n Actually, the simpler approach for the footer FAQ: store the entire FAQ as HTML in one locale key per language (e.g., `help.faqHtml`), rendered via `v-html`. The build pipeline produces this HTML from the combined markdown. --- ## Pipeline: Makefile A Makefile at the project root (`Makefile`) handles building the help content for all locales. ### Targets ```makefile # ── Help System ────────────────────────────────────────────── help-build: help-combine help-translate help-convert help-install help-combine: @echo "Combining ./docs/help/*.md into a single FAQ markdown..." @cat ./docs/help/general.md \ ./docs/help/user-roles.md \ ./docs/help/login-and-passwords.md \ ./docs/help/file-uploads.md \ ./docs/help/properties.md \ ./docs/help/tickets.md \ ./docs/help/expenses.md \ ./docs/help/running-expenses.md \ ./docs/help/vendors.md \ ./docs/help/leases.md \ ./docs/help/tenants.md \ ./docs/help/payments.md \ ./docs/help/invoices.md \ ./docs/help/chat-assistant.md \ ./docs/help/auto-billing.md \ ./docs/help/search-and-filters.md \ ./docs/help/csv-export.md > ./docs/help/faq-combined.md @echo "Combined FAQ written to ./docs/help/faq-combined.md" help-translate: @echo "Generating translated versions for all locales..." @node ./scripts/translate-help.js help-convert: @echo "Converting markdown to HTML snippets..." @node ./scripts/convert-help.js help-install: @echo "Copying help locale content into ./ui/src/locales/..." @cp ./build/help-en.json ./ui/src/locales/help-en.generated.json @cp ./build/help-de.json ./ui/src/locales/help-de.generated.json @cp ./build/help-es.json ./ui/src/locales/help-es.generated.json @cp ./build/help-fr.json ./ui/src/locales/help-fr.generated.json @echo "Help content installed. Merge into locale files or import as sidecar files." .PHONY: help-build help-combine help-translate help-convert help-install ``` ### Script: scripts/translate-help.js This script: 1. Reads `./docs/help/faq-combined.md` 2. For each of the 6 locales (en, de, es, fr, sq, hr): - If the locale is `en`, copies verbatim (source language) - For other locales, calls an AI translation API (e.g., OpenAI, DeepL, or a local model) to translate each section 3. Outputs a JSON file per locale with the structure: ```json { "help": { "title": "Help & FAQ", "searchPlaceholder": "Search help...", "selectTopic": "Select a topic from the list", "showFaq": "📖 FAQ", "hideFaq": "Hide FAQ", "topic": { "general": "General", "user-roles": "User Roles", "login-and-passwords": "Login & Passwords", "file-uploads": "File Uploads", "properties": "Properties", "tickets": "Tickets", "expenses": "Expenses", "running-expenses": "Running Expenses", "vendors": "Vendors", "leases": "Leases", "tenants": "Tenants", "payments": "Payments", "invoices": "Invoices", "chat-assistant": "Chat Assistant", "auto-billing": "Auto-Billing", "search-and-filters": "Search & Filters", "csv-export": "CSV Export" }, "content": { "general": "
What is Property Portal?
...
", "properties": "
...
", ... }, "faqHtml": "

General

...
" } } ``` ### Script: scripts/convert-help.js This script: 1. Uses `marked` (or similar) to convert each section's markdown to HTML 2. Wraps the Q&A content in semantic HTML classes 3. Outputs the locale JSON with `help.content.{section}` HTML strings and a combined `help.faqHtml` ### Installation The generated JSON files are sidecar files (e.g., `help-en.generated.json`) that should be: - Option A: Imported and merged into the main locale files at build time - Option B: Imported side-by-side in `i18n/index.js` and deep-merged into the messages object Option A is cleaner for production. Add an npm script: ```json { "scripts": { "help:build": "make help-build", "help:merge": "node scripts/merge-help-locales.js", "build": "npm run help:build && npm run help:merge && vite build" } } ``` The merge script reads each `help-{locale}.generated.json` and merges its `help` key into the corresponding `{locale}.json` locale file under a new `help` namespace. --- ## i18n Locale Structure ### New Keys in Each Locale JSON ```json { "help": { "title": "Help & FAQ", "searchPlaceholder": "Search help topics...", "selectTopic": "Select a topic from the list", "showFaq": "❓ Help & FAQ", "hideFaq": "Hide FAQ", "viewFullFaq": "View full FAQ in footer", "topic": { "general": "General", "user-roles": "User Roles", "login-and-passwords": "Login & Passwords", "file-uploads": "File Uploads", "properties": "Properties", "tickets": "Tickets", "expenses": "Expenses", "running-expenses": "Running Expenses", "vendors": "Vendors", "leases": "Leases", "tenants": "Tenants", "payments": "Payments", "invoices": "Invoices", "chat-assistant": "Chat Assistant", "auto-billing": "Auto-Billing", "search-and-filters": "Search & Filters", "csv-export": "CSV Export" }, "group": { "general": "General", "features": "Features", "system": "System" }, "content": { "general": "
...
", "user-roles": "...", "login-and-passwords": "...", "file-uploads": "...", "properties": "...", "tickets": "...", "expenses": "...", "running-expenses": "...", "vendors": "...", "leases": "...", "tenants": "...", "payments": "...", "invoices": "...", "chat-assistant": "...", "auto-billing": "...", "search-and-filters": "...", "csv-export": "..." }, "faqHtml": "

General

..." } } ``` ### Modified Locale Imports (i18n/index.js) ```js import { createI18n } from 'vue-i18n' import en from '../locales/en.json' import es from '../locales/es.json' import de from '../locales/de.json' import fr from '../locales/fr.json' // Deep-merge helper function deepMerge(target, source) { const result = { ...target } for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = deepMerge(result[key] || {}, source[key]) } else { result[key] = source[key] } } return result } // Merge help content into each locale import helpEn from '../locales/help-en.generated.json' import helpEs from '../locales/help-es.generated.json' import helpDe from '../locales/help-de.generated.json' import helpFr from '../locales/help-fr.generated.json' const messages = { en: deepMerge(en, helpEn), es: deepMerge(es, helpEs), de: deepMerge(de, helpDe), fr: deepMerge(fr, helpFr), } export const i18n = createI18n({ legacy: false, locale: 'en', fallbackLocale: 'en', messages, }) ``` --- ## Navigation Additions ### ControlPanel.vue — Help button Added as the last item in the main nav section (before the admin sections): ```html mdi-help-circle {{ t('nav.help') }} ``` ### AppHeader.vue — Help icon For unauthenticated users (next to About link) and authenticated users (next to the email): ```html mdi-help-circle-outline ``` ### App.vue — Help drawer integration ```html ``` --- ## Files to Create | File | Purpose | |------|---------| | `ui/src/components/HelpDrawer.vue` | The right-side help drawer component | | `ui/src/composables/useHelpContent.js` | Composable for filtering/searching topics | | `scripts/translate-help.js` | Node.js script to translate help content | | `scripts/convert-help.js` | Node.js script to convert markdown to HTML | | `scripts/merge-help-locales.js` | Node.js script to merge generated help into locale files | | `Makefile` (or appended to existing) | Build targets for help pipeline | ## Files to Modify | File | Change | |------|--------| | `ui/src/App.vue` | Add `` component, wire `@toggle-help` events | | `ui/src/components/AppHeader.vue` | Add help icon button, emit `toggle-help` | | `ui/src/components/ControlPanel.vue` | Add help nav item, emit `toggle-help` | | `ui/src/components/AppFooter.vue` | Add collapsible FAQ accordion | | `ui/src/i18n/index.js` | Import and merge generated help locale files | | `ui/src/locales/en.json` | Add `nav.help` key (plus other locales) | | `package.json` | Add npm scripts for help pipeline | --- ## Content Groups for the Help Drawer TOC ```js const helpGroups = [ { labelKey: 'help.group.general', topics: [ { slug: 'general', labelKey: 'help.topic.general' }, { slug: 'user-roles', labelKey: 'help.topic.user-roles' }, { slug: 'login-and-passwords', labelKey: 'help.topic.login-and-passwords' }, { slug: 'file-uploads', labelKey: 'help.topic.file-uploads' }, ] }, { labelKey: 'help.group.features', topics: [ { slug: 'properties', labelKey: 'help.topic.properties' }, { slug: 'tickets', labelKey: 'help.topic.tickets' }, { slug: 'expenses', labelKey: 'help.topic.expenses' }, { slug: 'running-expenses', labelKey: 'help.topic.running-expenses' }, { slug: 'vendors', labelKey: 'help.topic.vendors' }, { slug: 'leases', labelKey: 'help.topic.leases' }, { slug: 'tenants', labelKey: 'help.topic.tenants' }, { slug: 'payments', labelKey: 'help.topic.payments' }, { slug: 'invoices', labelKey: 'help.topic.invoices' }, ] }, { labelKey: 'help.group.system', topics: [ { slug: 'chat-assistant', labelKey: 'help.topic.chat-assistant' }, { slug: 'auto-billing', labelKey: 'help.topic.auto-billing' }, { slug: 'search-and-filters', labelKey: 'help.topic.search-and-filters' }, { slug: 'csv-export', labelKey: 'help.topic.csv-export' }, ] } ] ``` --- ## Responsive Behaviour | Breakpoint | Help Drawer | Footer FAQ | |------------|-------------|------------| | `lg`+ (1264+) | 380px drawer, comfortable layout | Full accordion, 3-column FAQ grid | | `md` (960–1263) | 340px drawer | Accordion, 2-column FAQ grid | | `sm` (600–959) | Full-width overlay (100vw) | Accordion, 1-column | | `xs` (<600) | Full-width overlay, compact TOC list hides labels, icons only | Accordion, 1-column, smaller text | --- ## Accessibility - Help drawer: `role="complementary"`, `aria-label="Help and FAQ"` - Topic list: `role="navigation"`, `aria-label="Help topics"` - FAQ accordion uses native Vuetify a11y (panel headers are buttons with `aria-expanded`) - Focus trap inside the drawer when open (Vuetify's navigation-drawer handles this) - All icons have `aria-hidden="true"` with text labels alongside --- ## Implementation Order 1. **Scripts first** — Write `scripts/convert-help.js` and `scripts/translate-help.js` so the build pipeline works 2. **Locale keys** — Add the `help.*` translation keys to all 4 locale JSON files 3. **HelpDrawer.vue** — Build the drawer component 4. **App.vue + AppHeader.vue + ControlPanel.vue** — Wire the toggle events 5. **Makefile** — Create the build targets 6. **i18n/index.js** — Add the deep-merge for help content 7. **AppFooter.vue** — Add the collapsible FAQ accordion 8. **npm scripts** — Wire into `package.json` 9. **Test** — Verify all 4 locales, responsive breakpoints, drawer open/close, search filtering # Invoices What are Invoices? Invoices are automatically generated and billed to a tenant. This is for rent or for a recurring expense. When are Invoices generated? There are two occasions, one is every night the system will automatically detect if rent is due based on the lease term and period. For more details see (link) leases. The second occasion is when a recurring expense is to be paid by the tenant. These are for things like water or electricity. Once the expense has been set to "Paid by tenant" the system will automatically generate an invoice to the tenant. Can I download a PDF of an invoice? Yes. On both the Invoices and Leases pages, click the red PDF icon on any row. The system generates a PDF on the fly with Property Sage branding and all the invoice details, then downloads it automatically. # Leases What is a Lease? A Lease is a rental agreement between you (the property manager/owner) and a tenant for a specific property. It captures the terms of the rental — who the tenant is, which property they are renting, the start and end dates of the agreement, the rent amount and payment period, the security deposit, and whether rent invoices should be auto-generated. Lease documents like signed contracts can be attached as files. How do I create a lease? Go to the Leases page and click "Add Lease." The creation form has several sections: - **Property & Tenant**: Select the property being leased and the tenant from your tenant list. Both are required. - **Lease Term**: Set the start date and end date of the lease agreement. - **Financial Details**: Enter the periodic rent amount, select the currency (defaults to EUR), choose the billing period in days (options typically include 14, 30, 45, 60, or 90 days), enter the security deposit amount, and toggle Auto-Billing on if you want rent invoices generated automatically. - **Documents**: Upload the signed lease agreement or any related documents (PDF, DOC, or image files). How do I view and manage leases? The Leases page displays a data table with columns for Property Name, Tenant Name, Rent amount, Auto-Billing status (shown as a check icon if enabled), number of documents, and action buttons. Expand any row to see the deposit, billing period, start date, and end date. Summary cards at the top show the total number of leases, the average rent, and the total deposit amount across all leases. How do I edit a lease? Unlike other modules, leases do not have a dedicated edit page. Click the pencil icon on any lease row to open an inline dialog where you can manage the lease documents — add new files or remove existing ones. The financial terms (rent, deposit, dates, period) are not editable through this dialog. If you need to change lease terms, you would typically end the current lease and create a new one. How do I delete/deactivate a lease? Leases use soft-delete. Click the "Deactivate" button in the expanded row of any lease. A confirmation dialog asks if you are sure — deactivating a lease removes it from the active leases list but keeps the record in the database. Only Company Admins and above can deactivate a lease. Deactivated leases can be reactivated if needed. What is Auto-Billing and how does it work? Auto-Billing is a feature that automatically generates rent invoices for a lease on a set schedule. When you enable Auto-Billing on a lease and set a billing period (e.g., 30 days for monthly rent), a nightly service checks if any new billing periods have completed and creates Pending invoices accordingly. For 30-day periods, it uses calendar months; for other periods (14, 45, 60, 90 days), it divides the elapsed time by the period length. The system is smart about duplicates — it checks if an invoice already exists for that lease and period before creating a new one, so running it multiple times is safe. How do lease documents work? When creating or editing a lease, you can upload documents like the signed rental contract. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Documents are stored on the server filesystem, and the file path is saved in the database. On the leases list page, a chip shows the document count (e.g., "2 docs") — click it to see a menu of files available for download. What filters are available on the Leases page? You can filter by Property (select from your properties), Tenant (select from your tenants), Status (Active/Inactive/All), Auto-Billing (Any/Yes/No), and End Date range (from/to). A text search box lets you search across property name, tenant name, rent, deposit, and period. Can I download invoices from the Leases page? Yes. Click the red PDF icon on any lease row to generate and download a PDF invoice with Property Sage branding and all the invoice details. Can I export leases to CSV? Yes. Click the "CSV" button to download all currently displayed leases (respecting your active filters) as a CSV file. What happens when a lease ends? When the end date of a lease is reached, the auto-billing service stops generating new invoices for that lease. The lease remains in the system so you can reference it later. On your dashboard, the "Leases Ending Soon" widget shows leases that are within 30 days of their end date, helping you plan renewals or find new tenants. # Login and Passwords Why won't my password work when I try to log in? Passwords are hashed before being sent, so plain text will not work. The web UI handles this automatically. Just type your normal password into the login form and it will work. Can I change my own password? Can an admin reset mine? Yes to both. Any logged-in user can change their own password from their account settings. Company Admins can reset another user's password without needing the old one. # Payments What are Payments? Payments record when a tenant has paid an invoice. This to help track and record that an invoice payment has been settled. What happens when I create a payment? The linked invoice status automatically changes to Paid. What if I delete a payment? If you delete a payment, which Company Admins can do, the invoice status goes back to Pending. What happens when I mark a recurring expense as paid by tenant? The system automatically creates a Pending invoice for that tenant. It looks up the active lease on that property, grabs the vendor details, and sets the due date 15 days out. Can I edit or delete an expense that has already been paid? No. Once an expense is marked Paid, it is locked. You cannot update or delete it. # Properties What is a Property in Property Portal? A Property is the core entity in the system — a building, apartment, house, condo, or any real estate unit you manage. Each property stores detailed information including its name, address (street, city, state, postal code, country, GPS coordinates, and national ID), physical details (square meters, bedrooms, bathrooms, total rooms, floor number, elevator count, parking spaces), and financial details for rental, purchase, or sale scenarios. You can also attach photos and documents to each property. How do I create a new property? Navigate to the Properties page and click the "Add Property" button. You will be taken to a form where you can enter the property name (required), address details, physical specifications, and optionally fill in rental pricing, purchase information, or sale asking prices. You can also upload photos (which get automatically compressed and resized to about 500KB) and attach documents like title deeds or inspection reports. Once you save, the property is created and automatically assigned to your managed list. The property's status (e.g., "For Rent," "Vacant," "For Sale") and structure type (e.g., "Apartment," "House," "Condo") are selected from lookup lists managed by your company admin. How do I view my properties? The All Properties page shows a card-based layout with each property displaying its cover photo, name, status (shown as a colored chip), key stats (bedrooms, bathrooms, square meters), and formatted price. Click on any property card to see its full detail page, which includes a photo carousel, the complete address, description, and tabs showing recent expenses and recent tickets linked to that property. Documents attached to the property are shown as downloadable icons. How do I edit a property? From the property detail page, click the "Edit" button. This opens the property form pre-filled with all existing data. You can update any field — change the status, adjust rental pricing, upload new photos or documents, remove existing ones, or update address details. Changes are saved immediately when you submit the form. How do I delete a property? Only Company Admins and above can delete a property. Navigate to the property detail page and use the delete option. Important: Property deletion is a hard delete — the record is permanently removed from the database along with all its associated photos and document files from the server. There is no undo. Consider deactivating the property by changing its status instead if you may need the data later. What filters can I use on the Properties page? You can filter properties by Status (multi-select from available statuses like "For Rent" or "Vacant"), City (text search), Bedrooms (select from 1 to 4+), Minimum Square Meters, Parking (Yes/No), Elevator (Yes/No), and Structure Type (e.g., Apartment, House). All filters combine together — for example, you can find all 2-bedroom apartments with parking in a specific city. How do property photos work? When you add photos to a property, the server automatically compresses them to a target size of around 500KB by reducing quality and scaling dimensions. The first photo you add becomes the cover photo (displayed first on the property card and detail page). You can change which photo is the cover. Photos are stored as JPEG files on the server, and only the file path is saved in the database. When you view the property, the photos are loaded and displayed in the browser. What property documents can I upload? You can attach any document files to a property — PDFs, Word documents, images, or other file types. These appear in a documents section on the property detail page. Click on any document to download it. Documents are stored on the server filesystem, not in the database. # Prospects What is a Prospect? A Prospect is a potential client or lead in your sales pipeline. It tracks someone who may become a tenant — from first contact through viewing, offer, and closing. Prospects store contact details, pipeline stage, follow-up dates, notes (interactions), tasks, commission info, lead source, and tags. How do I create a prospect? Go to the Prospects page and click "Add Prospect." Fill in the first name (required) and last name (required), plus email, phone, and property (all optional). You can also set the pipeline stage (defaults to "New"), a next follow-up date, lead source (e.g. Zillow, Referral, Open House), tags (e.g. Investor, First-time Buyer), and commission details (estimated price, rate, and type). What is the pipeline? The pipeline tracks where a prospect is in the sales process. There are six stages: New, Contacted, Viewing, Offer, Closed Won, and Closed Lost. You change the stage on the prospect's detail page. The pipeline section shows the current stage and lets you update it along with the next follow-up date. What is Lead Source? Lead Source tracks where the prospect came from — Zillow, Realtor.com, Referral, Open House, Social Media, Walk-in, Website, Cold Call, Existing Client, or Other. You set it when creating a prospect and can change it on the edit details page. The source is shown in the metrics bar on the prospect detail page and can be filtered on the prospects list. What are Tags? Tags let you categorize prospects with labels like Investor, First-time Buyer, Relocating, Hot Lead, or Nurturing. You can add custom tags by typing in the tags field — press Enter to add each tag. Tags are shown as chips on the prospect detail page and in the expanded row on the list page. You can filter prospects by tag via the API. How do I view and manage prospects? The Prospects page shows a data table with columns for Name, Email, Phone, Stage, Source, Property, and Next Follow-up. Summary cards at the top show Total, Open, and Won counts. You can search by name, email, or phone, and filter by source. Click the edit icon on any row to go to the prospect's detail page. What can I do on the prospect detail page? The detail page shows the prospect's profile (name, email, phone, property), metrics bar (stage, source, next follow-up, email, phone), contact details, tags, pipeline controls (stage and follow-up date), follow-up sequence enrollment, tasks, and interaction history. You can log notes (call, email, meeting, note), manage tasks with due dates, enroll in a follow-up sequence, and convert the prospect to a tenant. What are Follow-Up Sequences? Follow-Up Sequences are reusable templates for automated follow-up plans. Each sequence has a name, description, and ordered steps. Each step has a title, day offset (days from enrollment), and activity type (call, email, meeting, note). You create sequences on the Follow-Up Sequences page (accessible from the Prospects list). To use a sequence, select it on a prospect's detail page and click "Enroll" — the first task is created automatically. Click "Next Step" to advance through the sequence and create the next task. Click "Unenroll" to remove the prospect from the sequence. How do I convert a prospect to a tenant? On the prospect detail page, click the "Convert" button (visible when the prospect is in an open stage: New, Contacted, Viewing, or Offer). This creates a new Tenant from the prospect's contact details, sets the prospect's stage to Closed Won, and records the conversion. The tenant is available immediately on the Tenants page. What filters are available on the Prospects page? You can filter by name (searches both first and last name), email, phone, source, and stage. The API also supports filtering by property, tasks due status, overdue tasks, and active status. How does commission tracking work? Each prospect can have an estimated price, commission rate, and commission type (Percent, Months of rent, or Fixed amount). The reports page shows a weighted commission forecast that multiplies the commission amount by the stage win probability (New 10%, Contacted 25%, Viewing 50%, Offer 75%). Can the chat assistant answer questions about prospects? Yes. You can ask the chat about prospect counts by stage, prospects at a property, upcoming follow-ups, and prospect contact details. For example: "how many prospects do I have", "show prospects at Oak Street", "who has a follow-up soon", or "find contact info for Jane Smith". # Public Page (Marketplace) What is the Marketplace? The Marketplace is the public side of the portal — the pages at /public/countries/ (a country index) and /public/your-company-name (your company's public showcase). It lets anyone browse for-rent and for-sale properties and contact your agency without logging in. It works like a mini website for your agency. How do I enable a public page? A Company Admin opens Company Admin → Marketplace in the Control Panel and picks a public URL name (for example, bavaria-immobilien). The system checks the name is available before saving. Your page is then live at /public/your-name. Once set, the public name is permanent and cannot be changed or disabled — choose carefully. Which properties show on my public page? Only properties with a For Rent or For Sale status. Each property also has a "Publicly listed" toggle that is on by default — switch it off on a property to hide that listing from the Marketplace. Properties with a Sold or Rented status are automatically de-listed from the Marketplace. What data is shown publicly? Only safe listing info: photos, property name, city, country, price, size, bedrooms, bathrooms, parking, and elevator. Your street address, GPS coordinates, national IDs, and internal notes are never shown. How do leads work? Visitors fill in a contact form on the page (name, email, phone, message, and a consent checkbox). Each submission becomes a prospect in your CRM with stage "new" and source "public-website", so it appears in your prospects list and upcoming follow-ups. Is there a country page? Yes. /public/countries/your-country-code (for example /public/al for Albania) lists public properties from all companies that have enabled a public page in that country. Where can visitors browse all countries? The landing page links to /public/countries, an index that lists every country with available public listings (flag, name, and listing count). Each entry opens that country's page. The country menu in the public header jumps to the same pages. Can visitors search the public pages? Yes. Both the company page and the country page have a search bar in the header. On a company page it searches only that company's listings; on a country page it searches only that country's listings. The For rent / For sale buttons narrow the same results further. Can visitors switch country or language on the public pages? Yes. Country pages have a country menu (with flags and listing counts) that jumps to another country's page, and a language menu (English, Deutsch, Shqip, Hrvatski, Türkçe, ქართული, Čeština, Srpski) that re-labels the page instantly. The logo in the top corner links back to the landing page. Can visitors get back to a country page? Yes. A company page shows a "Back to " pill above the hero title that returns to the country page for that listing's country. The bottom of the hero image also fades smoothly into the page background. Is there a property detail page? Yes. Clicking any property card on a company or country page opens a dedicated page at /public/property/ with a photo carousel at the top, the price, an overview grid (property type, bedrooms, bathrooms, size, floor, parking, elevator), the description, and breadcrumbs (Home → Browse properties by country → Country → Agency → Property). A clear "I'm interested" button opens the same contact form as the agency page — each submission becomes a prospect in your CRM. The detail page shows the same safe info as the rest of the Marketplace: never your street address, GPS, national IDs, or documents. # Search and Filters What filters can I use on each page? On the Leases page you can filter by Property, Tenant, Status, Auto-Billing, and End Date range. On Payments you can filter by Payment Method, Min or Max Amount, and Date range. On Properties you can filter by Type, City, Bedrooms, Size, Parking, Elevator, and Structure. On Tenants you can filter by Name, Email, Phone, and Active status. On Vendors you can filter by Name, Vendor Type, Property, and Account. Can I combine multiple filters? Yes. All active filters combine together. For example, on the Leases page you can filter by a specific property with auto-billing enabled and an end date within a certain range all at once. # Tenants What is a Tenant? A Tenant is a person who rents a property from you. The system stores their basic contact information (first and last name, email, phone number), keeps track of whether they are currently active, and lets you attach documents related to them (like identification documents or guarantor forms). Tenants are linked to properties through leases. How do I create a tenant? Go to the Tenants page and click "Add Tenant." Fill in the tenant's first name (required) and last name (required), plus their email and phone number (optional). You can also upload documents like a copy of their ID, proof of income, or guarantor forms. Accepted file types are PDF, DOC, DOCX, JPG, JPEG, and PNG. Once saved, the tenant becomes available to link to leases. How do I view and manage tenants? The Tenants page shows a data table with columns for Name, Email, Phone, Active status (shown as a green or red chip), and Document count. Summary cards at the top display Total tenants, Active count, and Inactive count. Click the document count chip on any row to see a menu of uploaded files that you can download. What you see depends on your role: Company Admins see every tenant in the company, while Users only see tenants linked (through leases) to properties assigned to them. How do I edit a tenant? Click the pencil icon on any tenant row. An inline dialog opens where you can update the email, phone number, toggle the active status, and manage documents — upload new files or remove existing ones. The tenant's first and last names are preserved from the original creation and are sent back unchanged with the update. How do I delete a tenant? Tenants use soft-delete — clicking delete deactivates the tenant by setting them as inactive. The record stays in the database with all its data, so you can reference historical leases and documents. Deactivated tenants are hidden from the default view but can be revealed using the active filter. To permanently remove a tenant, database access would be needed. Deactivated tenants can be reactivated by an admin editing the record and toggling active back on. What filters are available on the Tenants page? You can filter by name (searches both first and last name), email, and phone — all case-insensitive text searches. By default, only active tenants are shown. The API supports filtering by active status (true, false, or all), but the current UI defaults to showing only active tenants. How do tenant documents work? When you add documents to a tenant record, they are saved to the server filesystem under `./files/tenant-documents/` and only the file path is stored in the database. On the tenants list page, the document count is shown as a clickable chip — click it to see a list of document names and download individual files. Documents can be uploaded when creating a tenant or added later through the edit dialog. How do tenants relate to leases? Tenants are linked to properties through leases. When you create a lease, you select a tenant and a property, forming the rental agreement. A tenant can have multiple leases (e.g., if they rent multiple properties or have renewed their lease over time). The tenant record itself does not store lease information — you find a tenant's leases by looking at the Leases page filtered by that tenant. # Tickets What is a Ticket? A Ticket records an issue or maintenance request for a property. Think of it as a support ticket for a physical location — something like "Leaky faucet in unit 3" or "Broken elevator in the lobby." Each ticket is linked to a specific property and tracks what the problem is, how severe it is, what its current status is, and when it needs to be resolved by. How do I create a ticket? Go to the Tickets page and click "Add Ticket." You will need to select the property the issue relates to (from a list of your properties), choose a status (e.g., Open, In Progress), set the severity (Critical, High, Medium, or Low), enter a description of the issue, and pick a due date for when it should be resolved. You can optionally set a resolved date if the issue is already fixed. Once created, the ticket appears in the list and on the property's detail page. How do I view and manage tickets? The Tickets page shows a data table with columns for Property, Status, Due Date, and Severity. The status is color-coded — closed/resolved tickets show green, in-progress ones show blue, open ones show orange. If a due date has passed, it turns red and bold so overdue items stand out. You can expand any row to see the full description and who created the ticket. At the top of the page, summary cards show your total tickets, how many are open, how many are high/critical severity, and how many are due within the next 7 days. How do I update a ticket? Click the pencil icon on any row in the tickets table. An inline edit dialog opens where you can change the description, status, severity, and due date. The system logs who made the change and when. If you resolve an issue, change the status to "Closed" and optionally set the resolved date. How do I delete a ticket? Only Company Admins and above can delete tickets. Ticket deletion is a hard delete — the record is permanently removed. Regular users do not see a delete option. If a ticket is no longer relevant, consider changing its status to "Closed" instead of deleting it. What filters are available on the Tickets page? You can filter by property, status (by exact status or by slug like "open"/"in_progress"), severity, who created the ticket, and date ranges for when the ticket was created, when it's due, or when it was resolved. You can also toggle between showing tickets from the last 30 days (Recent) or all historical tickets. A text search box lets you search across all fields on the client side. What are the default ticket statuses? The system comes with three default ticket statuses: Open, In Progress, and Closed. Your company admin can create additional statuses or rename existing ones through the company settings. The status determines how tickets are counted on the dashboard summary cards. Can I attach files to a ticket? No. Tickets are text-based issue trackers and do not support file or receipt attachments. If you need to attach documentation to a cost, use the Expenses feature instead. # User Roles What user roles are there? There are two roles you will encounter day to day. **Company Admin** has full access within their own company and can manage users. **User** handles day-to-day operations for the properties they are assigned to. What can a User see versus a Company Admin? For properties, Company Admins see all properties in their company while Users only see the ones explicitly assigned to them. For tickets, expenses, payments, leases, invoices, and recurring expenses, Users only see records for properties in their managed list while Company Admins see everything in their company. The same rule applies to prospects (the CRM) and tenants: Company Admins see all prospects and all tenants in the company, while Users only see prospects and tenants linked to properties they manage. Can a User create records? Yes. Users can add new properties (which are automatically assigned to them), tenants, vendors, and prospects for the properties they manage. They can also add/edit tickets, expenses, payments, leases, and recurring expenses — but only for properties in their managed list. Do invoices follow the same rule? Yes. Users only see invoices (pending, overdue, and historical) for properties in their managed list, and fetching a single invoice outside that list is denied. Company Admins see every invoice in their company. What can a Company Admin manage? A Company Admin manages everything in their own company: all records for any property, user management (adding users and changing roles between Company Admin and User), lookup lists (property statuses, structure types, etc.), and the Marketplace public page settings. Does the AI assistant respect the same visibility? Yes. The AI assistant (Chat) answers questions scoped to what the caller can see — Company Admins get company-wide answers, while Users only get answers about their managed properties and the prospects, tenants, and records linked to them. # Vendors What is a Vendor? A Vendor is a company or individual that provides services or supplies to your properties. This includes plumbers, electricians, cleaning services, utility companies, landscapers, and any other service provider you work with. Each vendor record stores contact information, account details, and what type of vendor they are. How do I create a vendor? Go to the Vendors page and click "Add Vendor." You will need to provide the vendor's name (required) and select their vendor type (e.g., "Plumber," "Electrician," "Utility Company") from the vendor types list. You can optionally link the vendor to a specific property, add an account number, phone numbers (up to two), email address, website, and a contact person's name. A description field lets you add any notes about the vendor. How do I view and manage vendors? The Vendors page shows a data table with columns for Name, Type, Property, Account, and Active status. Expand any row to see full contact details including phone numbers, email, website, contact person, and description. Summary cards at the top show Total vendors, Active count, Inactive count, and how many have account numbers on file. How do I edit a vendor? Click the pencil icon on any vendor row. An inline dialog opens where you can modify the description, vendor type, account number, phone numbers, email, website, contact person, and toggle the active status. All changes are saved immediately. How do I delete a vendor? Vendors use soft-delete — clicking delete deactivates the vendor by setting them as inactive rather than removing them from the database. The vendor record stays in the system but is hidden from the default view. Only Company Admins and above can deactivate a vendor. To permanently remove a vendor, an admin would need database access. Deactivated vendors can be reactivated by editing them and toggling the active switch back on. What filters are available on the Vendors page? You can filter by name, vendor type, property, account number, phone numbers, email, website, and contact person. All filters are case-insensitive text searches. A search box provides client-side filtering across name, account, phones, email, and contact fields. How do vendors relate to expenses? Vendors are primarily linked to recurring expenses. When you create a recurring expense, you select which vendor provided the service. If that expense is marked as "Paid by Tenant," the vendor's name and type are included in the auto-generated invoice notes so the tenant knows who the charge is from. Regular (one-time) expenses do not link to vendors.