expression field added in document editor. a few plans in plans folder
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
user: when both tare and gross are filled out, net should be calculated and saved (at backend). doc_number should be generated and assigned to the vehicle record. how will be the document number generated, what is better, use the vehicle id or a generator?
|
||||
Claude’s Plan
|
||||
Plan: Auto-calculate net + assign doc_number when both tare and gross are set
|
||||
Context
|
||||
One vehicle record = one weighing trip. When both tare and gross are recorded, the backend should
|
||||
automatically compute net = gross - tare, stamp net_date/net_user, and assign a unique
|
||||
document number formatted as YYYY-NNNNNN (year + 6-digit sequential counter, resetting per year).
|
||||
No frontend change is needed — the net and doc_number come back in the existing SSE broadcast.
|
||||
|
||||
Files to Modify
|
||||
File Change
|
||||
backend/api/models.py Add DocumentCounter model
|
||||
backend/vehicles/models.py Add doc_number field to Vehicle
|
||||
backend/api/serializers.py Add net, net_date, net_user_name, doc_number to VehicleSerializer
|
||||
backend/api/views.py Update set_tare and set_gross to compute net + doc_number
|
||||
new migration in api For DocumentCounter
|
||||
new migration in vehicles For doc_number on Vehicle
|
||||
Step 1 — Add DocumentCounter model (backend/api/models.py)
|
||||
|
||||
class DocumentCounter(models.Model):
|
||||
year = models.IntegerField(unique=True)
|
||||
last_number = models.IntegerField(default=0)
|
||||
Add a helper function in the same file (or in views.py, but models.py keeps it reusable):
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
def generate_doc_number(year):
|
||||
with transaction.atomic():
|
||||
counter, _ = DocumentCounter.objects.select_for_update().get_or_create(
|
||||
year=year, defaults={'last_number': 0}
|
||||
)
|
||||
counter.last_number += 1
|
||||
counter.save()
|
||||
return f"{year}-{counter.last_number:06d}"
|
||||
Step 2 — Add doc_number to Vehicle (backend/vehicles/models.py)
|
||||
|
||||
doc_number = models.CharField(max_length=20, null=True, blank=True, unique=True)
|
||||
Place after the net_user field.
|
||||
|
||||
Step 3 — Update VehicleSerializer (backend/api/serializers.py)
|
||||
Add to the VehicleSerializer:
|
||||
|
||||
|
||||
net_user_name = serializers.CharField(source='net_user.username', read_only=True, default=None)
|
||||
Add to fields list:
|
||||
|
||||
|
||||
'net', 'net_date', 'net_user', 'net_user_name', 'doc_number'
|
||||
Add to read_only_fields:
|
||||
|
||||
|
||||
'net', 'net_date', 'net_user', 'net_user_name', 'doc_number'
|
||||
Step 4 — Update set_tare and set_gross (backend/api/views.py)
|
||||
Extract a helper to avoid duplication:
|
||||
|
||||
|
||||
def _maybe_finalize(vehicle, user):
|
||||
"""If both tare and gross are now set, compute net and assign doc_number."""
|
||||
if vehicle.tare is not None and vehicle.gross is not None and vehicle.net is None:
|
||||
vehicle.net = vehicle.gross - vehicle.tare
|
||||
vehicle.net_date = timezone.now()
|
||||
vehicle.net_user = user
|
||||
vehicle.doc_number = generate_doc_number(timezone.now().year)
|
||||
vehicle.save()
|
||||
Call _maybe_finalize(vehicle, request.user) at the end of both set_tare and set_gross,
|
||||
before the SSE broadcast and serializer response (so the response already contains net + doc_number).
|
||||
|
||||
Import generate_doc_number from api.models.
|
||||
|
||||
Step 5 — Migrations
|
||||
|
||||
python manage.py makemigrations api # DocumentCounter
|
||||
python manage.py makemigrations vehicles # doc_number on Vehicle
|
||||
python manage.py migrate
|
||||
Verification
|
||||
Create a vehicle with no tare/gross → net and doc_number are null
|
||||
Set tare → net still null (only one weight set)
|
||||
Set gross → net is calculated, net_date and net_user are set, doc_number is "2026-000001"
|
||||
Second vehicle completes → doc_number is "2026-000002"
|
||||
Race condition test (optional): two simultaneous requests → both get unique sequential numbers
|
||||
Frontend SSE update reflects net and doc_number automatically (no frontend change needed)
|
||||
@@ -0,0 +1,75 @@
|
||||
user:
|
||||
let's enhance the users
|
||||
- admin users can manage the users. add or remove (mark as delete) users, change user permissions.
|
||||
user permissions are:
|
||||
- can measure
|
||||
- can manually measure
|
||||
- can manage entities
|
||||
- can edit documents
|
||||
make the changes to the backend then make the necessary changes to frontend
|
||||
|
||||
Claude’s Plan
|
||||
Plan: Enhanced User Management with Granular Permissions
|
||||
Context
|
||||
The current User model has a coarse role field (employee/viewer) and an is_admin boolean. The app needs granular permissions so admins can control exactly what each user can do. Additionally, admins need a UI to create, update, and soft-delete users.
|
||||
|
||||
New permissions to add:
|
||||
|
||||
can_measure – can set tare/gross weights
|
||||
can_manually_measure – can manually enter measurements
|
||||
can_manage_entities – can create/edit/delete vehicles
|
||||
can_edit_documents – can create/edit reports
|
||||
Backend Changes
|
||||
1. backend/api/models.py
|
||||
Add 4 boolean fields to the User model (default False):
|
||||
|
||||
|
||||
can_measure = models.BooleanField(default=False)
|
||||
can_manually_measure = models.BooleanField(default=False)
|
||||
can_manage_entities = models.BooleanField(default=False)
|
||||
can_edit_documents = models.BooleanField(default=False)
|
||||
Keep existing role and is_admin fields intact.
|
||||
|
||||
2. backend/api/serializers.py
|
||||
Add the 4 new fields to both UserSerializer and UserDetailSerializer fields lists.
|
||||
|
||||
3. backend/api/views.py
|
||||
Add a custom DRF permission class IsAdminUser (checks request.user.is_admin)
|
||||
Apply it to UserViewSet for all actions except me and change_password (which stay IsAuthenticated)
|
||||
Override destroy to do soft-delete (instance.is_active = False; instance.save()) instead of actual deletion — no data loss
|
||||
4. Migration
|
||||
python manage.py makemigrations && python manage.py migrate
|
||||
|
||||
Frontend Changes
|
||||
5. frontend/src/components/Users/UserManager.jsx (new file)
|
||||
Single-page admin UI with:
|
||||
|
||||
Table of all users (username, role, is_active, 4 permission checkboxes)
|
||||
"Add User" button → inline expandable form (username, password, permissions)
|
||||
Per-row: Edit (toggle permissions inline) and Deactivate/Reactivate (soft-delete toggle)
|
||||
Matches existing app style (similar panel pattern to Main.jsx)
|
||||
6. frontend/src/App.jsx
|
||||
Add route /users → <UserManager />, guarded so only currentUser.is_admin can access it (redirect to / otherwise).
|
||||
|
||||
7. frontend/src/components/Header.jsx
|
||||
Add "User Manager" nav button, visible only when currentUser?.is_admin === true (matches existing "Report Editor" / "Nomenclature Manager" pattern).
|
||||
|
||||
Critical Files
|
||||
backend/api/models.py – User model
|
||||
backend/api/serializers.py – UserSerializer, UserDetailSerializer
|
||||
backend/api/views.py – UserViewSet
|
||||
frontend/src/App.jsx – routing
|
||||
frontend/src/components/Header.jsx – nav
|
||||
frontend/src/components/Users/UserManager.jsx – new component
|
||||
Patterns to Reuse
|
||||
ChangePasswordOverlay.jsx style for modal/overlay
|
||||
api.get/post/patch/delete('/api/users/...') calls from services/api.js
|
||||
useAuth() hook to read currentUser.is_admin
|
||||
Two-panel layout pattern from Main.jsx
|
||||
Verification
|
||||
Backend: Run python manage.py makemigrations api && python manage.py migrate — should succeed
|
||||
Backend: GET /api/users/ with non-admin JWT → 403; with admin JWT → 200
|
||||
Backend: DELETE /api/users/{id}/ → user.is_active becomes False, user still in DB
|
||||
Frontend: Admin user sees "User Manager" button in header; non-admin does not
|
||||
Frontend: Admin can create a user with chosen permissions, edit permissions, and deactivate
|
||||
Frontend: Created user can log in and existing permission flags appear in /api/users/me/ response
|
||||
@@ -0,0 +1,472 @@
|
||||
ask:Explore Report Editor elements and inspector
|
||||
Now let me design the implementation plan based on this exploration.
|
||||
Now let me read a few critical files to verify the plan details before writing the final plan.
|
||||
Now I have everything I need. Let me write the final plan.
|
||||
|
||||
Claude’s Plan
|
||||
Plan: Expression Field Element for Report Editor
|
||||
Context
|
||||
The Report Editor currently supports DBTextElement for displaying individual data-bound field values. Users need the ability to compute derived values — e.g., gross - tare, IF(gross > 0, gross - tare, 0) — directly in the report. No expression/formula capability exists today.
|
||||
|
||||
Approach
|
||||
Add a new element type ExpressionElement (type: 'expr') that stores a formula string and evaluates it in preview mode. In design mode it shows the raw expression. The expression editor is embedded inline in the ObjectInspector (no modal needed).
|
||||
|
||||
Files to Create
|
||||
1. frontend/src/components/ReportEditor/utils/expressionEvaluator.js
|
||||
Standalone recursive-descent parser/evaluator. No eval() or Function() constructor.
|
||||
|
||||
Grammar:
|
||||
|
||||
|
||||
expression = comparison
|
||||
comparison = additive (CMP additive)? ← only one comparator per IF condition
|
||||
additive = multiplicative (('+' | '-') multiplicative)*
|
||||
multiplicative = unary (('*' | '/') unary)*
|
||||
unary = '-' unary | primary
|
||||
primary = NUMBER | STRING | FIELD_REF | 'IF' '(' expr ',' expr ',' expr ')' | '(' expr ')'
|
||||
Token types: NUMBER, STRING, FIELD_REF ([...]), IDENT (for IF), OP, CMP, COMMA, LPAREN, RPAREN, EOF
|
||||
|
||||
Public API:
|
||||
|
||||
|
||||
export function evaluateExpression(expression, resolveField)
|
||||
// resolveField('[vehicle.gross]') → actual value (string/number)
|
||||
// Returns computed value, '#ERR' on parse error, '#DIV/0' on division by zero
|
||||
The evaluator receives a resolveField(bracketed) closure — not raw data — so it stays decoupled from React context and is independently testable.
|
||||
|
||||
Field resolution inside evaluator: Numeric fields become Number(value) for arithmetic; string fields stay as strings. Arithmetic on strings falls back to string concatenation (same as spreadsheet behavior).
|
||||
|
||||
Error handling: entire call wrapped in try/catch → returns '#ERR' on any failure.
|
||||
|
||||
2. frontend/src/components/ReportEditor/ExpressionField.jsx
|
||||
Render component for the canvas. Modelled directly on DBTextField.jsx (129 lines).
|
||||
|
||||
Same imports: useReportData, useBandContext, resolveDBTextValue, ResizeHandles, useElementDrag, useElementSelection, getElementStyle
|
||||
|
||||
Design mode: displayContent = element.expression || '(expr)'
|
||||
|
||||
Preview mode:
|
||||
|
||||
|
||||
function buildResolver(reportData, parentBandId, currentBandData) {
|
||||
return (bracketed) => {
|
||||
const path = bracketed.slice(1, -1).trim(); // strip [ and ]
|
||||
const dotIdx = path.indexOf('.');
|
||||
if (dotIdx === -1) {
|
||||
return resolveDBTextValue(reportData, '', path, parentBandId, { currentBandData });
|
||||
}
|
||||
return resolveDBTextValue(reportData, path.slice(0, dotIdx), path.slice(dotIdx + 1), parentBandId, { currentBandData });
|
||||
};
|
||||
}
|
||||
const rawValue = evaluateExpression(element.expression, buildResolver(...));
|
||||
const displayContent = formatValue(rawValue, element.dataType || 'general');
|
||||
formatValue is copied locally from DBTextField.jsx (same function verbatim).
|
||||
|
||||
CSS class: expr-field (blue-violet tint rgba(103, 126, 234, 0.05) to distinguish from green db-text-field).
|
||||
|
||||
isUnresolved = !element.expression.
|
||||
|
||||
3. frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx
|
||||
Inline panel component used inside ObjectInspector for expr elements. Uses useRef for the textarea to support insert-at-cursor.
|
||||
|
||||
UI layout:
|
||||
|
||||
|
||||
[ Textarea showing raw expression — editable directly ]
|
||||
|
||||
Insert Field: [ select dropdown ▼ ] [ Insert ]
|
||||
|
||||
Operators: [ + ] [ - ] [ * ] [ / ] [ ( ] [ ) ]
|
||||
|
||||
[ IF( , , ) ]
|
||||
|
||||
Comparisons: [ = ] [ != ] [ > ] [ < ] [ >= ] [ <= ]
|
||||
|
||||
[ Clear ]
|
||||
|
||||
Data Type: [ general ▼ ] Alignment: [ left ▼ ]
|
||||
Insert-at-cursor logic:
|
||||
|
||||
|
||||
const insertAtCursor = (text) => {
|
||||
const ta = textareaRef.current;
|
||||
const start = ta.selectionStart, end = ta.selectionEnd;
|
||||
const next = expression.slice(0, start) + text + expression.slice(end);
|
||||
onUpdate({ expression: next });
|
||||
requestAnimationFrame(() => {
|
||||
ta.selectionStart = ta.selectionEnd = start + text.length;
|
||||
ta.focus();
|
||||
});
|
||||
};
|
||||
Available fields list: Built using the same band-detection logic as ObjectInspector does for dbtext — detect parentBand, resolve bandDataArray, call getAvailableFields(). Fields displayed as vehicle.gross, vehicle.tare (or just gross when inside a band). These are the values inserted between [ and ].
|
||||
|
||||
Props: { element, onUpdate, reportData, allElements }
|
||||
|
||||
Files to Modify
|
||||
4. frontend/src/components/ReportEditor/models/Element.jsx
|
||||
Add ExpressionElement class immediately after DBTextElement class (line ~158):
|
||||
|
||||
|
||||
export class ExpressionElement extends Element {
|
||||
constructor({ id, x, y, width = 10, height = 1, expression = '', dataType = 'general', alignment = 'left' }) {
|
||||
super({ id, type: 'expr', x, y });
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.expression = expression;
|
||||
this.dataType = dataType;
|
||||
this.alignment = alignment;
|
||||
}
|
||||
getBounds() { return { minX: this.x, minY: this.y, maxX: this.x + this.width - 1, maxY: this.y + this.height - 1 }; }
|
||||
getDimensions() { return { width: this.width, height: this.height }; }
|
||||
toJSON() { return { ...super.toJSON(), width: this.width, height: this.height, expression: this.expression, dataType: this.dataType, alignment: this.alignment }; }
|
||||
}
|
||||
Add expr: ExpressionElement to the elementClasses map inside fromJSON (line ~93–101).
|
||||
|
||||
5. frontend/src/components/ReportEditor/EditorCanvas.jsx
|
||||
Three surgical changes:
|
||||
|
||||
Import: import ExpressionField from './ExpressionField';
|
||||
In renderElement() switch — add after dbtext branch:
|
||||
|
||||
} else if (element.type === 'expr') {
|
||||
return <ExpressionField {...commonProps} previewMode={previewMode} parentBandId={parentBandId} />;
|
||||
}
|
||||
In handleCanvasClick — add after addDBText branch:
|
||||
|
||||
} else if (toolMode === 'addExpr') {
|
||||
onAddElement({ id: `expr-${Date.now()}`, type: 'expr', x: col, y: row, width: 10, height: 1, expression: '', dataType: 'general', alignment: 'left' });
|
||||
}
|
||||
6. frontend/src/components/ReportEditor/Toolbar.jsx
|
||||
Add one button after the DB Text button:
|
||||
|
||||
|
||||
<button className={`toolbar-button ${toolMode === 'addExpr' ? 'active' : ''}`}
|
||||
onClick={() => onToolChange('addExpr')} title="Add Expression Field">
|
||||
fx
|
||||
</button>
|
||||
7. frontend/src/components/ReportEditor/ObjectInspector.jsx
|
||||
Two changes:
|
||||
|
||||
Import ExpressionEditorPanel at top.
|
||||
Add section after the dbtext block (after line ~73):
|
||||
|
||||
{element.type === 'expr' && (
|
||||
<ExpressionEditorPanel element={element} onUpdate={onUpdate} reportData={reportData} allElements={allElements} />
|
||||
)}
|
||||
8. frontend/src/components/ReportEditor/elements.css
|
||||
Add .expr-field block (identical structure to .db-text-field but with blue-violet rgba(103,126,234,...) colors instead of green).
|
||||
|
||||
9. frontend/src/components/ReportEditor/ObjectInspector.css
|
||||
Add .expr-btn-row utility class for the operator button rows:
|
||||
|
||||
|
||||
.expr-btn-row { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: 6px; }
|
||||
.expr-btn-row button { padding: 2px 6px; font-size: 11px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; background: #fff; }
|
||||
.expr-btn-row button:hover { background: #e8eeff; border-color: #667eea; }
|
||||
ResizeHandles coverage
|
||||
ResizeHandles.jsx handles resize for elements with width/height. Check whether it already covers any type with these properties generically, or if 'dbtext' is explicitly named — if the latter, add || element.type === 'expr' to those conditions.
|
||||
|
||||
Verification
|
||||
Toolbar: Click fx button → canvas cursor changes, click canvas → ExpressionElement created (type 'expr' in elements array).
|
||||
Design mode: Selected expr element shows raw expression string (e.g., [vehicle.gross] - [vehicle.tare]).
|
||||
Inspector - field insert: Open ConfigPanel with vehicle data, select expr element, pick vehicle.gross from dropdown, click Insert → [vehicle.gross] appended at cursor in textarea.
|
||||
Inspector - operators: Click + → + inserted at cursor.
|
||||
Preview mode (math): Expression [vehicle.gross] - [vehicle.tare] with gross=28500, tare=8500 → displays 20000.
|
||||
Preview mode (IF): IF([vehicle.tare] > 0, [vehicle.gross] - [vehicle.tare], 0) → displays 20000.
|
||||
Error cases: Invalid expression → shows #ERR; division by zero → #DIV/0.
|
||||
Band context: Expr element inside a band resolves [fieldPath] from current band row.
|
||||
Persistence: Save report → load report → expr element reloads with expression intact.
|
||||
Resize: Resize handles appear and work the same as on dbtext elements.
|
||||
|
||||
|
||||
claude created a skill to help him implement the plan:
|
||||
|
||||
dispatch skill
|
||||
Base directory for this skill: c:\dev_projects\ScalesApp\.claude\skills\dispatch
|
||||
|
||||
You are the **development orchestrator** for this project. Do not implement changes yourself — always dispatch to the appropriate specialized agent.
|
||||
|
||||
## Routing
|
||||
|
||||
| Work involves... | Agent |
|
||||
|-------------------------------------------------------------------|-------------------|
|
||||
| `frontend/` — React, JSX, hooks, components, state, CSS | `react-expert-dev` |
|
||||
| `backend/` — Django, Python, models, views, serializers, migrations | `python-pro` |
|
||||
| Both layers | Both, in parallel |
|
||||
|
||||
## Rules
|
||||
|
||||
1. Each agent is called **exactly once** — never call the same agent twice.
|
||||
2. For tasks that span both layers, launch both agents in a **single message** using two parallel Task tool calls.
|
||||
3. Every agent prompt must include the no-subagent constraint (see below).
|
||||
|
||||
## Dispatch Steps
|
||||
|
||||
1. **Analyze** — identify which directories and layers the task touches.
|
||||
2. **Compose** a detailed prompt for each relevant agent:
|
||||
- Full task description and context
|
||||
- Relevant file paths to examine
|
||||
- Clear acceptance criteria
|
||||
- Append this constraint verbatim at the end:
|
||||
|
||||
> **CONSTRAINT: Do NOT use the Task tool. Do not spawn any subagents. Complete all work directly using your available file and shell tools.**
|
||||
|
||||
3. **Launch** via the Task tool:
|
||||
- Frontend → `subagent_type: react-expert-dev`
|
||||
- Backend → `subagent_type: python-pro`
|
||||
- Both → two calls in a single message (parallel)
|
||||
|
||||
4. **Report** — after all agents complete, summarize:
|
||||
- What each agent did
|
||||
- Any required follow-up steps (e.g., run migrations, restart services)
|
||||
- Any issues or pre-existing problems the agents flagged
|
||||
|
||||
|
||||
ARGUMENTS: Add Expression Field element to the Report Editor. This is a frontend-only change.
|
||||
|
||||
## Summary
|
||||
|
||||
Add a new `ExpressionElement` (type: `'expr'`) to the report editor that evaluates math expressions in preview mode.
|
||||
|
||||
## Files to Create
|
||||
|
||||
### 1. `frontend/src/components/ReportEditor/utils/expressionEvaluator.js`
|
||||
|
||||
Standalone recursive-descent parser/evaluator. No `eval()` or `Function()`.
|
||||
|
||||
Grammar:
|
||||
- `expression = comparison`
|
||||
- `comparison = additive (CMP additive)?` — one comparator per IF condition
|
||||
- `additive = multiplicative (('+' | '-') multiplicative)*`
|
||||
- `multiplicative = unary (('*' | '/') unary)*`
|
||||
- `unary = '-' unary | primary`
|
||||
- `primary = NUMBER | STRING | FIELD_REF | 'IF' '(' expr ',' expr ',' expr ')' | '(' expr ')'`
|
||||
|
||||
Token types: `NUMBER`, `STRING`, `FIELD_REF` (text wrapped in `[...]`), `IDENT` (for `IF`), `OP` (`+`, `-`, `*`, `/`), `CMP` (`=`, `!=`, `>`, `<`, `>=`, `<=`), `COMMA`, `LPAREN`, `RPAREN`, `EOF`
|
||||
|
||||
Public API:
|
||||
```js
|
||||
export function evaluateExpression(expression, resolveField)
|
||||
// resolveField('[vehicle.gross]') → actual value
|
||||
// Returns computed value, '#ERR' on error, '#DIV/0' on div by zero
|
||||
```
|
||||
|
||||
`resolveField` is a closure passed in — the evaluator doesn't know about React or reportData.
|
||||
|
||||
Field refs in arithmetic: `Number(resolveField(ref))` for arithmetic operations; if NaN, treat as 0.
|
||||
All errors are caught and return `'#ERR'`. Division by zero returns `'#DIV/0'`.
|
||||
|
||||
Implement the tokenizer, recursive-descent parser building an AST, and AST evaluator as three separate internal functions. Export only `evaluateExpression`.
|
||||
|
||||
### 2. `frontend/src/components/ReportEditor/ExpressionField.jsx`
|
||||
|
||||
Render component. Model it exactly on `DBTextField.jsx` (at `frontend/src/components/ReportEditor/DBTextField.jsx` — read it first).
|
||||
|
||||
Differences from DBTextField:
|
||||
- Imports `evaluateExpression` from `./utils/expressionEvaluator`
|
||||
- Design mode: `displayContent = element.expression || '(expr)'`
|
||||
- Preview mode: builds a `resolveField` closure using `resolveDBTextValue` from `./utils/dataResolver`, then calls `evaluateExpression(element.expression, resolveField)`
|
||||
- The resolveField closure: strips `[` and `]` from the bracketed path, splits on first `.` to get objectKey and fieldPath, then calls `resolveDBTextValue(reportData, objectKey, fieldPath, parentBandId, { currentBandData })`. If no `.` in the path, treat as band-relative (objectKey = `''`).
|
||||
- `isUnresolved = !element.expression`
|
||||
- CSS class: `expr-field` (not `db-text-field`)
|
||||
- Copies `formatValue` function verbatim from `DBTextField.jsx`
|
||||
- Display text inside span: `displayContent || '(No Expr)'`
|
||||
|
||||
### 3. `frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx`
|
||||
|
||||
Props: `{ element, onUpdate, reportData, allElements }`
|
||||
|
||||
Imports: `React`, `{ useState, useRef }`, `{ getAvailableFields, getAvailableObjects }` from `./utils/dataResolver`
|
||||
|
||||
Available fields logic (replicates the `dbtext` block in `ObjectInspector.jsx` lines 36-73):
|
||||
- Detect parentBand from allElements
|
||||
- If in a band, resolve bandDataArray and call `getAvailableFields(bandDataArray[0])` → fields are band-relative paths (no `objectKey.` prefix)
|
||||
- If not in a band: for each objectKey from `getAvailableObjects(reportData)`, call `getAvailableFields(reportData[objectKey])` and prefix each with `objectKey.` → produces `vehicle.gross`, `vehicle.tare`, etc.
|
||||
- Combine all into a flat `availableFields` array of strings
|
||||
|
||||
State: `const [selectedField, setSelectedField] = useState('')`
|
||||
Ref: `const textareaRef = useRef(null)`
|
||||
|
||||
Insert-at-cursor:
|
||||
```js
|
||||
const insertAtCursor = (text) => {
|
||||
const ta = textareaRef.current;
|
||||
if (!ta) { onUpdate({ expression: (element.expression || '') + text }); return; }
|
||||
const start = ta.selectionStart;
|
||||
const end = ta.selectionEnd;
|
||||
const current = element.expression || '';
|
||||
const next = current.slice(0, start) + text + current.slice(end);
|
||||
onUpdate({ expression: next });
|
||||
requestAnimationFrame(() => {
|
||||
ta.selectionStart = ta.selectionEnd = start + text.length;
|
||||
ta.focus();
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
UI (use existing inspector CSS classes where possible):
|
||||
```jsx
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Expression</div>
|
||||
|
||||
<textarea ref={textareaRef} value={element.expression || ''}
|
||||
onChange={(e) => onUpdate({ expression: e.target.value })}
|
||||
rows={3} style={{ width: '100%', fontFamily: 'monospace', fontSize: '11px', resize: 'vertical', boxSizing: 'border-box' }}
|
||||
/>
|
||||
|
||||
<div className="property-row">
|
||||
<select value={selectedField} onChange={e => setSelectedField(e.target.value)} style={{ flex: 1 }}>
|
||||
<option value="">-- field --</option>
|
||||
{availableFields.map(f => <option key={f} value={f}>{f}</option>)}
|
||||
</select>
|
||||
<button onClick={() => { if (selectedField) insertAtCursor(`[${selectedField}]`); }}>Insert</button>
|
||||
</div>
|
||||
|
||||
<div className="expr-btn-row">
|
||||
{['+', '-', '*', '/'].map(op => <button key={op} onClick={() => insertAtCursor(` ${op} `)}>{op}</button>)}
|
||||
<button onClick={() => insertAtCursor('(')}>(</button>
|
||||
<button onClick={() => insertAtCursor(')')}>)</button>
|
||||
</div>
|
||||
|
||||
<div className="expr-btn-row">
|
||||
<button onClick={() => insertAtCursor('IF( , , )')}>IF( , , )</button>
|
||||
</div>
|
||||
|
||||
<div className="expr-btn-row">
|
||||
{['=', '!=', '>', '<', '>=', '<='].map(op => <button key={op} onClick={() => insertAtCursor(` ${op} `)}>{op}</button>)}
|
||||
</div>
|
||||
|
||||
<div className="expr-btn-row">
|
||||
<button onClick={() => onUpdate({ expression: '' })}>Clear</button>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Data Type:</label>
|
||||
<select value={element.dataType || 'general'} onChange={e => onUpdate({ dataType: e.target.value })}>
|
||||
<option value="general">General</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="time">Time</option>
|
||||
<option value="datetime">Date & Time</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Alignment:</label>
|
||||
<select value={element.alignment || 'left'} onChange={e => onUpdate({ alignment: e.target.value })}>
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 4. `frontend/src/components/ReportEditor/models/Element.jsx`
|
||||
|
||||
Read the file first. Add `ExpressionElement` class after `DBTextElement` class. Same structure as `DBTextElement` but replaces `objectKey`/`fieldPath` with `expression`:
|
||||
```js
|
||||
export class ExpressionElement extends Element {
|
||||
constructor({ id, x, y, width = 10, height = 1, expression = '', dataType = 'general', alignment = 'left' }) {
|
||||
super({ id, type: 'expr', x, y });
|
||||
this.width = width; this.height = height;
|
||||
this.expression = expression; this.dataType = dataType; this.alignment = alignment;
|
||||
}
|
||||
getBounds() { return { minX: this.x, minY: this.y, maxX: this.x + this.width - 1, maxY: this.y + this.height - 1 }; }
|
||||
getDimensions() { return { width: this.width, height: this.height }; }
|
||||
toJSON() { return { ...super.toJSON(), width: this.width, height: this.height, expression: this.expression, dataType: this.dataType, alignment: this.alignment }; }
|
||||
}
|
||||
```
|
||||
Also add `expr: ExpressionElement` to the `elementClasses` map in `fromJSON`.
|
||||
|
||||
### 5. `frontend/src/components/ReportEditor/EditorCanvas.jsx`
|
||||
|
||||
Read the file first. Make 3 surgical changes:
|
||||
1. Add import: `import ExpressionField from './ExpressionField';`
|
||||
2. In `renderElement()` — add `expr` case after `dbtext` case (same props pattern as dbtext)
|
||||
3. In canvas click handler — add `addExpr` tool mode after `addDBText` (creates element with type `'expr'`, x, y, width=10, height=1, expression='', dataType='general', alignment='left')
|
||||
|
||||
### 6. `frontend/src/components/ReportEditor/Toolbar.jsx`
|
||||
|
||||
Read first. Add `fx` toolbar button after the DB Text button. Same pattern as other tool buttons. `toolMode === 'addExpr'` → active class.
|
||||
|
||||
### 7. `frontend/src/components/ReportEditor/ObjectInspector.jsx`
|
||||
|
||||
Read first. Two changes:
|
||||
1. Add import: `import ExpressionEditorPanel from './ExpressionEditorPanel';`
|
||||
2. After the `dbtext` block (around line 73), add:
|
||||
```jsx
|
||||
{element.type === 'expr' && (
|
||||
<ExpressionEditorPanel element={element} onUpdate={onUpdate} reportData={reportData} allElements={allElements} />
|
||||
)}
|
||||
```
|
||||
|
||||
### 8. `frontend/src/components/ReportEditor/elements.css`
|
||||
|
||||
Read first. Add `.expr-field` block after `.db-text-field` block. Identical structure but use blue-violet `rgba(103, 126, 234, ...)` colors instead of green.
|
||||
|
||||
### 9. `frontend/src/components/ReportEditor/ObjectInspector.css`
|
||||
|
||||
Read first. Add at the end:
|
||||
```css
|
||||
.expr-btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.expr-btn-row button {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
.expr-btn-row button:hover {
|
||||
background: #e8eeff;
|
||||
border-color: #667eea;
|
||||
}
|
||||
```
|
||||
|
||||
### 10. `frontend/src/components/ReportEditor/ResizeHandles.jsx`
|
||||
|
||||
Read first. Find where `'dbtext'` is mentioned and add `'expr'` alongside it — for resize handles support and pixel size calculation.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Read every file before modifying it
|
||||
- Match existing code style exactly
|
||||
- Do not add comments, docstrings, or cleanup unrelated code
|
||||
- The evaluator must use recursive-descent parsing only — no eval(), no Function(), no regex-based math evaluation
|
||||
|
||||
|
||||
|
||||
Expression Field — Done
|
||||
New files
|
||||
expressionEvaluator.js — Pure recursive-descent parser/evaluator (no eval). Supports +, -, *, /, IF(cond, a, b), comparisons (=, !=, >, <, >=, <=), field refs [path], numbers and strings.
|
||||
ExpressionField.jsx — Canvas element component. Design mode shows raw expression; preview mode evaluates it.
|
||||
ExpressionEditorPanel.jsx — Inline expression builder in the inspector.
|
||||
Modified files
|
||||
models/Element.jsx — Added ExpressionElement class + registered 'expr' in fromJSON
|
||||
EditorCanvas.jsx — Import + addExpr click handler + expr render case
|
||||
Toolbar.jsx — Added fx button
|
||||
ObjectInspector.jsx — Renders ExpressionEditorPanel for expr elements
|
||||
elements.css — Blue-violet .expr-field styles
|
||||
ObjectInspector.css — .expr-btn-row button styles
|
||||
ResizeHandles.jsx — 'expr' added alongside 'dbtext' in 3 places
|
||||
How to use
|
||||
Click the fx button in the toolbar
|
||||
Click on the canvas to place an expression element
|
||||
Select it — the Expression panel appears in the inspector
|
||||
Pick a field from the dropdown and click Insert, or type directly
|
||||
Use the operator buttons (+, -, *, /), IF( , , ), and comparison buttons to build the expression
|
||||
Switch to preview mode to see the computed result
|
||||
Expression syntax
|
||||
Example Result
|
||||
[vehicle.gross] - [vehicle.tare] Subtraction
|
||||
[vehicle.gross] * 0.1 Multiplication
|
||||
IF([vehicle.tare] > 0, [vehicle.gross] - [vehicle.tare], 0) Conditional
|
||||
([vehicle.gross] - [vehicle.tare]) / 1000 Grouped
|
||||
Reference in New Issue
Block a user