Forms
Bilko Forms Documentation
Current State: Native HTML forms with React state Validation: Client-side JavaScript validation (no schema validation yet) Future State: Zod schemas for validation, react-hook-form for form management
Forms Overview
graph TD
FORMS["Bilko Forms"]
FORMS --> IW["Invoice Wizard\n6-step multi-page form\n/invoices/new"]
FORMS --> EF["Expense Form\nDialog (modal)\n/expenses"]
FORMS --> ENF["Add Expense Page\nStandalone page\n/expenses/new"]
FORMS --> SF["Settings Forms\n6 sections\n/settings"]
FORMS --> AUTH["Auth Forms\nLogin + Register\n/login /register"]
IW --> IW1["Step 1: Customer"]
IW --> IW2["Step 2: Details"]
IW --> IW3["Step 3: Line Items"]
IW --> IW4["Step 4: Customization"]
IW --> IW5["Step 5: Preview"]
IW --> IW6["Step 6: Send"]
EF --> EF1["Amount + Currency"]
EF --> EF2["Category + Date"]
EF --> EF3["Vendor + Payment Method"]
EF --> EF4["Receipt Upload (placeholder)"]
EF --> EF5["Description (optional)"]
SF --> SF1["Company Profile"]
SF --> SF2["Tax & Compliance"]
SF --> SF3["Notification Preferences"]
SF --> SF4["Security Settings"]
AUTH --> AUTH1["Login\nemail + password"]
AUTH --> AUTH2["Register\nname, company, email, password, country"]
Invoice Creation Wizard (6-Step Multi-Page Form)
Route: /invoices/new
File: app/(dashboard)/invoices/new/page.tsx
Wizard State Machine
stateDiagram-v2
[*] --> Step1_Customer : navigate to /invoices/new
Step1_Customer : Step 1 — Customer Selection
Step1_Customer : Select existing customer (dropdown)
Step1_Customer : OR open Add Customer dialog
Step1_Customer : Validation: customer required
Step2_Details : Step 2 — Invoice Details
Step2_Details : invoiceNumber (auto-generated)
Step2_Details : issueDate, dueDate
Step2_Details : netTerms (Net 15/30/60 → auto-updates dueDate)
Step2_Details : currency (EUR/RSD/BAM)
Step3_LineItems : Step 3 — Line Items
Step3_LineItems : description (required), qty, unitPrice
Step3_LineItems : vatRate (0/10/17/20/25%)
Step3_LineItems : total auto-calculated (read-only)
Step3_LineItems : Validation: min 1 item with description
Step4_Customize : Step 4 — Customization
Step4_Customize : notes (optional textarea)
Step4_Customize : terms (optional textarea)
Step4_Customize : No validation required
Step5_Preview : Step 5 — Preview
Step5_Preview : Read-only invoice render
Step5_Preview : All data from previous steps
Step5_Preview : No validation
Step6_Send : Step 6 — Send / Save
Step6_Send : to (email, pre-filled from customer)
Step6_Send : subject, message (pre-filled template)
Step6_Send : sendCopy (checkbox)
Step6_Send : Save as Draft / Download PDF / Send Invoice
Step1_Customer --> Step2_Details : Next (customer selected)
Step1_Customer --> Step1_Customer : Next (no customer) — alert shown
Step2_Details --> Step3_LineItems : Next
Step2_Details --> Step1_Customer : Back
Step3_LineItems --> Step4_Customize : Next (has valid item)
Step3_LineItems --> Step3_LineItems : Next (no items) — alert shown
Step3_LineItems --> Step2_Details : Back
Step4_Customize --> Step5_Preview : Next
Step4_Customize --> Step3_LineItems : Back
Step5_Preview --> Step6_Send : Next
Step5_Preview --> Step4_Customize : Back
Step6_Send --> [*] : Send Invoice → alert + redirect /invoices
Step6_Send --> Step5_Preview : Back
Step6_Send --> [*] : Cancel → confirm dialog → /invoices
Form Structure
Step 1: Customer Selection
Fields:
- Customer (required)
- Type: Select (dropdown)
- Options: All customers from contacts (type='customer')
- Can add new customer via dialog
- Validation: Required before proceeding to step 2
Add Customer Dialog:
- Name (required)
- Type: Text
- Validation: Required
- Email (required)
- Type: Email
- Validation: Required, valid email format
- Phone (optional)
- Type: Tel
- Validation: None
- Tax ID (optional)
- Type: Text
- Validation: None
Validation:
- Alert shown if user tries to proceed without selecting customer
- Form submission triggers inline alert (no schema validation)
Step 2: Invoice Details
Fields:
-
Invoice Number
- Type: Text
- Default: Auto-generated (e.g., "INV-2026-009")
- Validation: None (can be edited)
-
Issue Date
- Type: Date
- Default: Today's date
- Validation: None
-
Due Date
- Type: Date
- Default: 30 days from issue date
- Validation: None
-
Net Terms (shortcut selector)
- Type: Select
- Options: Net 15, Net 30, Net 60
- Behavior: Auto-calculates due date when selected
- Validation: None
-
Currency
- Type: Select
- Options: EUR, RSD, BAM
- Default: EUR
- Validation: None
Behavior:
- Net terms selector auto-updates due date field
- All fields can be manually overridden
Step 3: Line Items
Repeating Fields (Line Items):
Each line item contains:
-
Description (required)
- Type: Text
- Placeholder: "Service or product description"
- Validation: At least one item must have description
-
Quantity
- Type: Number
- Default: 1
- Min: 1
- Validation: Positive number
-
Unit Price
- Type: Number
- Default: 0
- Min: 0
- Step: 0.01
- Validation: Non-negative
-
VAT Rate
- Type: Select
- Options: 0%, 10%, 17%, 20%, 25%
- Default: 20%
- Validation: None
-
Total (calculated, read-only)
- Type: Text (disabled input)
- Calculation:
quantity * unitPrice * (1 + vatRate/100) - Display: Formatted currency
Actions:
Totals Display (read-only):
- Subtotal (before VAT)
- VAT Total
- Grand Total (with VAT)
Validation:
- Alert shown if user tries to proceed with all empty descriptions
- Form submission requires at least one item with description
Step 4: Customization
Fields:
-
Notes (optional)
- Type: Textarea
- Default: "Thank you for your business!"
- Placeholder: "Add a note for your customer..."
- Validation: None
-
Terms (optional)
- Type: Textarea
- Default: "Payment due within 30 days."
- Placeholder: "Payment terms and conditions..."
- Validation: None
Behavior:
- Both fields are optional
- Default values pre-populated but can be cleared
Step 5: Preview (Read-Only)
No form fields. Displays formatted invoice preview with all data from previous steps.
Preview Elements:
- Invoice title ("INVOICE")
- From/To addresses
- Invoice number, date, due date
- Line items table
- Subtotal, VAT, Total
- Notes (if provided)
- Terms (if provided)
No validation. Step is purely visual review.
Step 6: Send/Save
Email Form:
-
To (required)
- Type: Email
- Default: Pre-filled with customer email
- Validation: Valid email format (no schema yet)
-
Subject (required)
- Type: Text
- Default: "Invoice {invoiceNumber}"
- Validation: Required
-
Message (required)
- Type: Textarea
- Default: Pre-filled template
- Rows: 6
- Validation: Required
-
Send Me a Copy (optional)
- Type: Checkbox
- Default: Unchecked
- Validation: None
- Save as Draft — Alert placeholder (no API)
- Download PDF — Alert placeholder (no API)
- Send Invoice — Alert + redirect to
/invoices(no API)
Validation:
- No schema validation
- Form submission triggers alert "Invoice sent!"
Line Item Data Flow
flowchart LR
DESC["description\n(text input)"]
QTY["quantity\n(number input)"]
PRICE["unitPrice\n(number input)"]
VAT["vatRate\n(Select: 0/10/17/20/25%)"]
QTY --> CALC["useMemo(totals)\nsubtotal = qty * price\nvatTotal = subtotal * rate\ntotal = subtotal + vatTotal"]
PRICE --> CALC
VAT --> CALC
CALC --> ROW_TOTAL["Row Total\n(read-only display)"]
CALC --> SUBTOTAL["Subtotal\n(sum of all rows)"]
CALC --> VAT_TOTAL["VAT Total\n(sum of all VAT)"]
CALC --> GRAND_TOTAL["Grand Total\n(subtotal + vatTotal)"]
DESC -->|"required"| VALID["Validation\nAt least 1 item\nwith description"]
Form State Management
Local State:
const [step, setStep] = useState(1)
const [customer, setCustomer] = useState<Contact | null>(null)
const [showAddCustomer, setShowAddCustomer] = useState(false)
const [invoiceDetails, setInvoiceDetails] = useState<InvoiceDetails>({
number: "INV-2026-009",
issueDate: "2026-02-20",
dueDate: "2026-03-22",
currency: "EUR"
})
const [lineItems, setLineItems] = useState<LineItem[]>([
{ description: "", quantity: 1, unitPrice: 0, vatRate: 20, total: 0 }
])
const [notes, setNotes] = useState("Thank you for your business!")
const [terms, setTerms] = useState("Payment due within 30 days.")
const [emailData, setEmailData] = useState({
to: "",
subject: "",
message: "",
sendCopy: false
})
No persistence: All state lost on page refresh or navigation away.
No Zod schemas: Validation is inline JavaScript (alert boxes).
Expense Form (Dialog)
Route: /expenses
Component: Dialog triggered by "Add Expense" button
Expense Form Flow
flowchart TD
BTN["'Add Expense' Button\nExpenses Page"]
BTN --> DLG["Dialog Opens\nisDialogOpen=true"]
DLG --> AMOUNT["Amount (number, required)"]
DLG --> CURRENCY["Currency (Select: EUR/RSD/BAM)"]
DLG --> CATEGORY["Category (Select, required)\nOffice/Travel/Meals/Utilities\nMarketing/Infrastructure\nSoftware/Professional Services"]
DLG --> DATE["Date (date input, required)"]
DLG --> VENDOR["Vendor (text, optional)"]
DLG --> PAYMENT["Payment Method (Select, optional)\nCash/Card/Bank Transfer"]
DLG --> RECEIPT["Receipt Upload\n(placeholder UI — no real upload)"]
DLG --> DESC["Description (text, optional)"]
AMOUNT --> SUBMIT["Save Expense button"]
CATEGORY --> SUBMIT
SUBMIT -->|"current"| CONSOLE["console.log(formData)\nDialog closes\nForm resets"]
SUBMIT -->|"future"| API["POST /api/expenses\nRefetch expense list"]
DLG --> CANCEL["Cancel button\nDialog closes\nForm resets"]
Form Fields
-
Amount (required)
- Type: Number
- Placeholder: "0.00"
- Validation: Required (no schema)
-
Currency (required)
- Type: Select
- Options: EUR, RSD, BAM
- Default: EUR
- Width: 24px (narrow select next to amount)
- Validation: None
-
Category (required)
- Type: Select
- Options: Office, Travel, Meals, Utilities, Marketing, Infrastructure, Software, Professional Services
- Placeholder: "Select category"
- Validation: Required
-
Date (required)
- Type: Date
- Default: Today's date
- Validation: None
-
Vendor (optional)
- Type: Text
- Placeholder: "Search vendor..."
- Validation: None
- Note: Not a searchable autocomplete yet — plain text input
-
Payment Method (optional)
- Type: Select
- Options: Cash, Card, Bank Transfer
(Note: no PayPal — dialog differs from /expenses/new page) - Placeholder: "Select method"
- Validation: None
-
Receipt (optional)
- Type: File upload (placeholder UI only)
- Display: Dashed border div with "📷 Upload or Drag" text
- Behavior: No actual upload implemented
- Validation: None
-
Description (optional)
- Type: Text
- Placeholder: "Additional notes..."
- Validation: None
Form Actions
- Cancel — Closes dialog, resets form
- Save Expense — Logs form data to console, closes dialog, resets form
No API submission. Form data not persisted.
No Zod schemas. Validation is JavaScript logic in form submit handler.
Add Expense Page (/expenses/new)
Route: /expenses/new
File: app/(dashboard)/expenses/new/page.tsx
Purpose: Standalone mobile-friendly add-expense page (alternative to the dialog on /expenses)
Form Flow
flowchart TD
NAV["Navigate to /expenses/new"]
NAV --> SCAN["Scan Receipt Button\n(Camera icon — no OCR yet)"]
NAV --> FORM["Manual Form Entry\n(below OR divider)"]
FORM --> AMOUNT["Amount (number, required)\nCurrency symbol prefix (€ / currency code)"]
FORM --> CATEGORY["Category (Select, required)\nOffice Supplies/Travel/Software/Infrastructure\nProfessional Services/Marketing/Utilities/Other"]
FORM --> DATE["Date (date, required)\nDefaults to today"]
FORM --> VENDOR["Vendor (text, optional)"]
FORM --> PAYMENT["Payment Method (Select, optional)\nBank Transfer/Credit-Debit Card/Cash/PayPal"]
FORM --> RECEIPT["Receipt (attach button\nno real upload yet)"]
FORM --> DESC["Description (textarea, optional)"]
FORM --> SAVE["Save button (top right)\nor 'Save Expense' button (bottom)\ncalls useExpenseStore.createExpense()"]
SAVE -->|success| REDIRECT["/expenses (router.push)"]
SAVE -->|error| ALERT["alert('Failed to save expense.')"]
Form Fields
Amount(required)Type: Number, step: 0.01Currency prefix:€for EUR, code for othersValidation: Required (if empty, save is disabled)
CurrencyType: Select inline with Amount fieldOptions: EUR, RSD, BAMDefault: EUR
Category(required)Type: SelectOptions: office, travel, software, infrastructure, professional, marketing, utilities, otherValidation: Required
Date(required)Type: DateDefault: Today's date
Vendor(optional)Type: TextPlaceholder: "Vendor name"
Payment Method(optional)Type: SelectOptions: Bank Transfer, Credit/Debit Card, Cash,PayPalNote: PayPal is an option here but NOT in the expenses dialog
Receipt(optional)Type: Button (no real upload — placeholder)Display: Paperclip icon + "Attach receipt"
Description(optional)Type: Textarea (native, not shadcn)Height: 122px fixedPlaceholder: "Add notes..."
Form Actions
Save (top right)+Save Expense (bottom button)— both calluseExpenseStore.createExpense(), redirect to/expensesBack arrow— navigates back to/expenses
Settings Forms
Route: /settings
File: app/(dashboard)/settings/page.tsx
Settings Navigation Flow
stateDiagram-v2
[*] --> Company : default section
Company : Company Profile
Company : name, legalForm, address, city\npostalCode, country, taxId\nbaseCurrency, fiscalYearStart
Users : Users & Roles
Users : User management table\n(name, email, role, status)\nInvite User button
Tax : Tax & Compliance
Tax : country (Serbia/BiH/Croatia)\nvatRegistered (checkbox)\nvatNumber (conditional)\nvatRate (conditional)\ncompliance reminder checkboxes
Integrations : Integrations
Integrations : Connected: Intesa Bank CSV, Email SMTP\nAvailable: Stripe, Fiken\nGoogle Sheets, Slack, DocuSeal
Notifications : Notification Preferences
Notifications : Email: paid, overdue, approved, synced\nIn-App: invoice updates, expense updates\nreconciliation matches
Security : Security Settings
Security : Enable 2FA button\nSession Timeout Select\nPassword Policy checkboxes\nAudit Log / Data Export / Delete Company
Company --> Users : sidebar nav click
Company --> Tax : sidebar nav click
Company --> Integrations : sidebar nav click
Company --> Notifications : sidebar nav click
Company --> Security : sidebar nav click
Users --> Company : sidebar nav click
Tax --> Company : sidebar nav click
Integrations --> Company : sidebar nav click
Notifications --> Company : sidebar nav click
Security --> Company : sidebar nav click
Company Profile Form
Fields:
-
Company Name (required)
- Type: Text
- Default: "SnowIT d.o.o."
-
Legal Form
- Type: Select
- Options: d.o.o., a.d., Preduzetnik
- Default: "d.o.o."
-
Address
- Type: Text
- Default: "Zmaja od Bosne"
-
City
- Type: Text
- Default: "Sarajevo"
-
Postal Code
- Type: Text
- Default: "71000"
-
Country
- Type: Text
- Default: "BiH"
-
Tax ID / PIB / JIB
- Type: Text
- Default: "4200000000"
-
Base Currency
- Type: Select
- Options: EUR, RSD, BAM
- Default: "EUR"
-
Fiscal Year Start
- Type: Select
- Options: Jan 1, Apr 1, Jul 1, Oct 1
- Default: "Jan 1"
Action:
- Save Changes — Alert placeholder (no API)
Validation: None (no required fields enforced)
Tax & Compliance Form
Fields:
-
Country
- Type: Select
- Options: Serbia, BiH, Croatia
- Default: "Serbia"
-
VAT Registered
- Type: Checkbox
- Default: Checked
-
VAT Number (conditional, shown only if VAT registered)
- Type: Text
- Default: "RS123456789"
- Placeholder: "Enter VAT number"
-
VAT Rate (conditional, shown only if VAT registered)
- Type: Select
- Options: 17% (BiH), 20% (Serbia), 25% (Croatia)
- Default: "20"
Compliance Reminders:
- VAT filing deadlines — Checkbox (default: checked)
- Annual tax returns — Checkbox (default: checked)
- Payroll tax deadlines — Checkbox (default: unchecked)
Action:
- Save Settings — Alert placeholder (no API)
Validation: None
Notification Preferences
Email Notifications:
- Invoice paid — Checkbox (default: checked)
- Invoice overdue — Checkbox (default: checked)
- Expense approved — Checkbox (default: unchecked)
- Bank account synced — Checkbox (default: checked)
In-App Notifications:
- Invoice updates — Checkbox (default: checked)
- Expense updates — Checkbox (default: checked)
- Reconciliation matches — Checkbox (default: unchecked)
Action:
- Save Preferences — Alert placeholder (no API)
Validation: None
Security Settings
Two-Factor Authentication:
Session Timeout:
- Type: Select
- Options: 15 minutes, 30 minutes, 1 hour, 4 hours
- Default: 30 minutes
Password Policy:
- Minimum 12 characters — Checkbox (default: checked)
- Require special characters — Checkbox (default: checked)
- Expire passwords after 90 days — Checkbox (default: unchecked)
Actions:
- View Audit Log — No functionality yet
- Request Data Export — No functionality yet
- Delete Company (Danger Zone) — No functionality yet
Validation: None
Auth Forms
Login Form (/login)
flowchart TD
LP["LoginPage\n/login"]
LP --> EMAIL["Email input\ntype=email, required"]
LP --> PASS["Password input\ntype=password, show/hide toggle"]
LP --> SUBMIT["Submit Button\n'Prijavite se'"]
SUBMIT --> STORE["useAuthStore.login(email, password)"]
STORE -->|"success"| DASH["router.push('/dashboard')"]
STORE -->|"error"| ERR["Error banner\n(red bg, border, message from store)"]
LP --> FORGOT["Forgot password link\n(no functionality yet)"]
LP --> REG["Link to /register"]
Register Form (/register)
Fields:
First Name(required)Last Name(required)Company(required)Email(required)Password(required, show/hide toggle)Country(required, Select)
Submit: Calls api.auth.register() → auto-login via useAuthStore.login() → redirect to /dashboard
Validation Architecture
flowchart LR
subgraph CURRENT["Current (Phase 1)"]
INLINE["Inline JS Validation\nalert() on submit\nno schema, no real-time feedback"]
INLINE --> INVOICE_V["Invoice Wizard\n• Step 1: if(!customer) alert()\n• Step 3: if(!descriptions) alert()"]
INLINE --> EXPENSE_V["Expense Form\n• if(!amount || !category) return"]
INLINE --> AUTH_V["Auth Forms\n• HTML required attribute\n• type=email browser validation"]
end
subgraph FUTURE["Future (Phase 2)"]
ZOD["Zod Schemas\ncentralized, type-safe, reusable"]
RHF["react-hook-form\nuseForm + zodResolver"]
ZOD --> RHF
RHF --> RT["Real-time Validation\nfield-level, on-change or on-blur"]
RHF --> EM["Error Messages\ninline under each field"]
RHF --> ES["Error State Styling\nred border + error text"]
end
Future Form Enhancements (Phase 2)
Zod Schema Validation
Planned: Replace inline validation with Zod schemas
Example (Invoice Wizard Step 1):
import { z } from 'zod'
const customerSchema = z.object({
id: z.string(),
name: z.string().min(1, "Customer name required"),
email: z.string().email("Valid email required"),
phone: z.string().optional(),
taxId: z.string().optional()
})
Benefits:
- Type-safe validation
- Reusable schemas for API/DB
- Better error messages
- Centralized validation logic
react-hook-form Integration
Planned: Replace useState with react-hook-form
Example (Expense Form):
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
const expenseSchema = z.object({
amount: z.number().positive("Amount must be positive"),
currency: z.enum(['EUR', 'RSD', 'BAM']),
category: z.string().min(1, "Category required"),
date: z.string(),
vendor: z.string().optional(),
paymentMethod: z.string().optional(),
description: z.string().optional()
})
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(expenseSchema)
})
Benefits:
- Automatic error handling
- Less boilerplate
- Better performance (no re-renders on every keystroke)
- Built-in dirty/touched state
Field-Level Validation
Planned: Real-time validation as user types
Example (Email Field):
<Input
{...register("email")}
type="email"
error={errors.email?.message}
/>
{errors.email && (
<span className="text-error text-sm">{errors.email.message}</span>
)}
Current State: No real-time validation, only on form submit.
Form Persistence
Planned: Save draft forms to localStorage
Use Cases:
- Invoice wizard state saved between page refreshes
- Expense form data saved if user closes dialog accidentally
Implementation:
// Save to localStorage on every state change
useEffect(() => {
localStorage.setItem('invoice-draft', JSON.stringify(invoiceState))
}, [invoiceState])
// Load from localStorage on mount
useEffect(() => {
const draft = localStorage.getItem('invoice-draft')
if (draft) setInvoiceState(JSON.parse(draft))
}, [])
File Upload (Receipt Attachment)
Current State: Placeholder UI only (dashed border div)
Future Implementation:
- Drag-and-drop file upload
- File size validation (max 5MB)
- File type validation (PDF, JPG, PNG)
- Preview uploaded file
- Remove uploaded file
- Upload to backend API
API Endpoint (planned):
POST /api/expenses/:id/receipt
Content-Type: multipart/form-data
Autocomplete/Search Fields
Current State: Plain text inputs
Future Enhancement (Vendor Field):
- Searchable dropdown
- Autocomplete from existing vendors
- Create new vendor inline
- Match by partial name
Library: Radix UI Combobox or react-select
Multi-Currency Conversion
Current State: User manually selects currency
Future Enhancement:
- Fetch live exchange rates
- Auto-convert amounts for display
- Store both original currency and base currency
- Show converted amounts in tooltips
Summary
Current Forms:
- Invoice Wizard (6-step) — Customer, Details, Line Items, Customization, Preview, Send
- Expense Form (
dialog on /expenses)dialog) — Amount, Category, Date, Vendor, Receipt, etc. Add Expense Page (/expenses/new) — standalone mobile-friendly form with Scan Receipt CTA- Company Profile — All company settings
- Tax & Compliance — VAT settings
- Notification Preferences — Email/in-app notification toggles
- Security Settings — 2FA, session timeout, password policy
Login Form — Email + password, auth via Zustand storeRegister Form — Name, company, email, password, country
Validation:
- Inline JavaScript (alert boxes)
for wizard and expense form HTMLrequiredattribute + browser validation for auth forms- No schema validation
- No real-time validation
- No error state styling
State Management:
- React useState for all forms
- No persistence (lost on refresh)
- No form libraries (native HTML forms)
Auth forms useuseAuthStorefrom Zustand (login/register)
Future (Phase 2):
- Zod schemas for validation
- react-hook-form for form management
- Field-level validation
- Form persistence (localStorage)
- File upload functionality
- Autocomplete/search fields
- API integration for submission