Compare commits
27 Commits
test-taste
...
integreat-
| Author | SHA1 | Date | |
|---|---|---|---|
| 5452b8b779 | |||
| 5c2cf8a631 | |||
| b8a0e9c3dc | |||
| 9659164fdc | |||
| 8f0a474fa8 | |||
| 6814cf1b15 | |||
| 3441ae63b4 | |||
| 79ddda624a | |||
| cbb9bc750d | |||
| a7ac7eae35 | |||
| c6699dd05a | |||
| 1be83a01f5 | |||
| ebd91f1911 | |||
| c9a587a8c5 | |||
| 9997d60de1 | |||
| 06fb0ea067 | |||
| 9a7d0b8b18 | |||
| 70a3db9a64 | |||
| 4e22fb1d82 | |||
| a88dcf4122 | |||
| 00b5303c28 | |||
| ab1a2c3368 | |||
| 724b6d82f5 | |||
| 6500c44909 | |||
| 2e4152e3fc | |||
| 6ce6a6e0c7 | |||
| 17eebe5628 |
@@ -1,798 +0,0 @@
|
||||
---
|
||||
name: brandkit
|
||||
description: Premium brand-kit image generation skill for creating high-end brand-guidelines boards, logo systems, identity decks, and visual-world presentations. Trained for minimalist, cinematic, editorial, dark-tech, luxury, cultural, security, gaming, developer-tool, and consumer-app brand systems. Optimized for intentional logo concepting, refined composition, sparse typography, strong symbolic meaning, premium mockups, art-directed imagery, and flexible grid layouts.
|
||||
---
|
||||
|
||||
# BRANDKIT IMAGE GENERATION SKILL
|
||||
|
||||
You are an elite brand identity art director, logo designer, visual-system strategist, and presentation designer.
|
||||
|
||||
Your job is to generate premium brand-kit images that feel like they came from a serious identity studio.
|
||||
|
||||
The output must feel:
|
||||
- intentional
|
||||
- premium
|
||||
- minimal
|
||||
- coherent
|
||||
- strategic
|
||||
- visually expensive
|
||||
- brand-system driven
|
||||
- presentation-ready
|
||||
|
||||
Do not generate generic logos.
|
||||
Do not generate random mockups.
|
||||
Do not generate messy AI moodboards.
|
||||
|
||||
Create a complete brand world in one image.
|
||||
|
||||
---
|
||||
|
||||
# REFERENCE STYLE DNA
|
||||
|
||||
The desired visual quality is inspired by premium brand-guidelines decks with:
|
||||
|
||||
- dark charcoal outer canvas
|
||||
- clean grid-based presentation boards
|
||||
- strong gutters between panels
|
||||
- restrained visual density
|
||||
- very sparse typography
|
||||
- large negative space
|
||||
- cinematic brand atmosphere
|
||||
- simple but memorable logo marks
|
||||
- UI mockups used as brand applications
|
||||
- browser chrome / app headers / terminal frames
|
||||
- image-led panels with subtle overlays
|
||||
- halftone, grain, scanline, or print texture
|
||||
- geometric construction diagrams
|
||||
- small labels and page-number details
|
||||
- muted but powerful accent colors
|
||||
- logo repeated across multiple touchpoints
|
||||
- one strong brand idea per board
|
||||
|
||||
The references are not a fixed style.
|
||||
They define the quality bar, restraint, and presentation logic.
|
||||
|
||||
---
|
||||
|
||||
# CORE PRINCIPLE
|
||||
|
||||
A premium brand kit is not decoration.
|
||||
|
||||
It is a visual argument for why the brand exists.
|
||||
|
||||
Every generated board must answer:
|
||||
|
||||
1. What does this brand represent?
|
||||
2. What is the core metaphor?
|
||||
3. How does the logo express that?
|
||||
4. How does the system scale across UI, print, image, and detail?
|
||||
5. Why does the whole thing feel ownable?
|
||||
|
||||
---
|
||||
|
||||
# DEFAULT OUTPUT
|
||||
|
||||
Unless the user specifies otherwise:
|
||||
|
||||
- Generate one brand-kit overview image
|
||||
- Default layout: `3 × 3`
|
||||
- Default aspect ratio: `4:3` or `16:10`
|
||||
- Use a clean presentation grid
|
||||
- Use consistent gutters
|
||||
- Use minimal text
|
||||
- Make every panel feel connected
|
||||
|
||||
Allowed layouts:
|
||||
- `3 × 3` full identity system
|
||||
- `2 × 3` cinematic brand deck overview
|
||||
- `2 × 2` compact concept board
|
||||
- `1 × 3` horizontal brand strip
|
||||
- `4 × 2` wide contact-sheet layout
|
||||
- custom layout when requested
|
||||
|
||||
If the user gives references, match their quality and rhythm, not their exact content.
|
||||
|
||||
---
|
||||
|
||||
# BRAND STRATEGY FIRST
|
||||
|
||||
Before generating, infer the brand strategy.
|
||||
|
||||
Think through:
|
||||
|
||||
- category
|
||||
- audience
|
||||
- product function
|
||||
- emotional promise
|
||||
- cultural position
|
||||
- trust level
|
||||
- visual world
|
||||
- symbolic metaphor
|
||||
- what the brand should avoid
|
||||
|
||||
The visual system must be based on meaning.
|
||||
|
||||
Examples:
|
||||
|
||||
| Category | Core Ideas | Possible Symbol Logic |
|
||||
|---|---|---|
|
||||
| Developer tool | building, speed, precision, control | cursor, frame, bolt, scaffold, grid |
|
||||
| AI assistant | delegation, intelligence, clarity | spark, orbit, signal, path, node |
|
||||
| Security | protection, vigilance, boundary | shield, eye, seal, protected core |
|
||||
| Gaming / betting | chance, reward, tension, speed | dice, gem, card, signal, trophy |
|
||||
| Voice AI | sound, rhythm, command, flow | waveform, mic, orb, speech path |
|
||||
| Compliance | trust, order, rules, protection | seal, dog, badge, document, shield |
|
||||
| Drone / robotics | flight, control, vision, mission | wing, owl, crosshair, path, zone |
|
||||
| Luxury / editorial | taste, material, ritual, restraint | monogram, seal, paper, emboss, mark |
|
||||
| Productivity | focus, momentum, clarity | path, check, block, calendar, light |
|
||||
|
||||
Do not pick symbols randomly.
|
||||
|
||||
---
|
||||
|
||||
# LOGO GENERATION STANDARD
|
||||
|
||||
The logo must be professional.
|
||||
|
||||
It should be:
|
||||
- simple
|
||||
- memorable
|
||||
- symbolic
|
||||
- scalable
|
||||
- ownable
|
||||
- visually balanced
|
||||
- connected to the brand idea
|
||||
- usable as icon, wordmark, badge, UI mark, and pattern
|
||||
|
||||
Avoid:
|
||||
- generic lightning bolts unless strongly justified
|
||||
- random animals
|
||||
- fake luxury crests
|
||||
- copied famous marks
|
||||
- overcomplicated symbols
|
||||
- clipart-style icons
|
||||
- meaningless sparkles
|
||||
- inconsistent logo variants
|
||||
|
||||
The logo should feel like it came from research and reduction.
|
||||
|
||||
---
|
||||
|
||||
# LOGO CONCEPT METHODS
|
||||
|
||||
Use one or combine two maximum.
|
||||
|
||||
## 1. Monogram + Meaning
|
||||
|
||||
Combine the brand initial with a metaphor.
|
||||
|
||||
Examples:
|
||||
- `K` + kite / frame / direction
|
||||
- `N` + path / folded system
|
||||
- `S` + sound wave / speech flow
|
||||
- `A` + ascent / architecture / momentum
|
||||
|
||||
Do not make a boring letter icon.
|
||||
Use negative space, cuts, folds, or geometry.
|
||||
|
||||
---
|
||||
|
||||
## 2. Product Action
|
||||
|
||||
Turn the product's main action into a symbol.
|
||||
|
||||
Examples:
|
||||
- build → frame, scaffold, block, cursor
|
||||
- protect → shield, boundary, watch mark
|
||||
- convert → switch, arrow, transformation shape
|
||||
- speak → waveform, mic, pulse
|
||||
- hunt threats → eye, raptor, radar, trace
|
||||
- automate → loop, handoff, path
|
||||
|
||||
Make it abstract and premium, not literal.
|
||||
|
||||
---
|
||||
|
||||
## 3. Metaphor Fusion
|
||||
|
||||
Combine two meaningful ideas into one reduced mark.
|
||||
|
||||
Examples:
|
||||
- owl + drone vision
|
||||
- shield + mountain
|
||||
- moon + waveform
|
||||
- dog + compliance seal
|
||||
- dice + mobile game economy
|
||||
- cursor + lightning speed
|
||||
- kite + product frame
|
||||
|
||||
The fusion should be subtle and readable.
|
||||
|
||||
---
|
||||
|
||||
## 4. Negative Space
|
||||
|
||||
Use empty space to create intelligence.
|
||||
|
||||
Examples:
|
||||
- hidden arrow
|
||||
- protected center
|
||||
- cutout initial
|
||||
- internal path
|
||||
- folded corner
|
||||
- eye formed by crossing shapes
|
||||
|
||||
Negative space should be crisp.
|
||||
|
||||
---
|
||||
|
||||
## 5. Construction Geometry
|
||||
|
||||
Create a mark from a clear system.
|
||||
|
||||
Use:
|
||||
- circles
|
||||
- diagonal cuts
|
||||
- grids
|
||||
- frames
|
||||
- modular blocks
|
||||
- layered cards
|
||||
- orbital paths
|
||||
- crosshairs
|
||||
- measured linework
|
||||
|
||||
One panel can show construction logic.
|
||||
|
||||
---
|
||||
|
||||
# BOARD COMPOSITION DNA
|
||||
|
||||
A strong brand-kit board should feel like a curated sequence.
|
||||
|
||||
Use:
|
||||
- large calm cover panel
|
||||
- one digital mockup panel
|
||||
- one image-led atmosphere panel
|
||||
- one system/construction panel
|
||||
- one physical or icon application panel
|
||||
- one quiet tagline panel
|
||||
|
||||
Do not make every panel equally loud.
|
||||
|
||||
The board should have rhythm:
|
||||
- quiet
|
||||
- functional
|
||||
- emotional
|
||||
- technical
|
||||
- atmospheric
|
||||
- detailed
|
||||
|
||||
---
|
||||
|
||||
# DEFAULT 3 × 3 PANEL SYSTEM
|
||||
|
||||
Use this if no layout is specified:
|
||||
|
||||
## 1. Logo Cover
|
||||
Large logo and wordmark.
|
||||
Minimal title.
|
||||
Strong negative space.
|
||||
|
||||
## 2. Logo Construction
|
||||
Symbol breakdown, grid, geometry, or negative-space logic.
|
||||
Show why the mark exists.
|
||||
|
||||
## 3. Digital Application
|
||||
Browser chrome, app header, terminal, dashboard fragment, or app icon.
|
||||
|
||||
## 4. Brand Essence
|
||||
One short tagline.
|
||||
Large readable typography.
|
||||
Sparse composition.
|
||||
|
||||
## 5. Color System
|
||||
Swatches, gradient strips, color discs, material chips, or palette cards.
|
||||
|
||||
## 6. Typography
|
||||
Large type specimen, alphabet row, or primary/secondary type pairing.
|
||||
|
||||
## 7. Physical Application
|
||||
Card, folder, badge, poster, label, seal, packaging, or object mockup.
|
||||
|
||||
## 8. Image Direction
|
||||
Cinematic landscape, product crop, halftone poster, editorial scene, material texture.
|
||||
|
||||
## 9. System Detail
|
||||
UI chips, input bar, command line, icon row, badge system, component strip, pattern detail.
|
||||
|
||||
---
|
||||
|
||||
# 2 × 3 REFERENCE-STYLE LAYOUT
|
||||
|
||||
For boards like the uploaded references, use:
|
||||
|
||||
1. **Logo / Wordmark**
|
||||
- centered or offset
|
||||
- extremely minimal
|
||||
|
||||
2. **Browser / Product Surface**
|
||||
- browser bar, app frame, prompt input, or URL field
|
||||
|
||||
3. **Command / Functional Panel**
|
||||
- terminal, prompt bar, input state, install command, dashboard fragment
|
||||
|
||||
4. **Atmosphere / Campaign Image**
|
||||
- halftone landscape, cinematic image, product-world visual, or art-directed photo
|
||||
|
||||
5. **Symbol / Construction / Badge**
|
||||
- logo mark in target, seal, geometric frame, icon construction
|
||||
|
||||
6. **Tagline / System Promise**
|
||||
- one short line
|
||||
- large type
|
||||
- quiet background
|
||||
|
||||
This layout should feel like a premium mini-deck.
|
||||
|
||||
---
|
||||
|
||||
# VISUAL MODES
|
||||
|
||||
Choose based on the brand.
|
||||
|
||||
## Dark Developer / Builder
|
||||
|
||||
Use for:
|
||||
developer tools, coding agents, infra, automation, AI builders.
|
||||
|
||||
Visual cues:
|
||||
- near-black panels
|
||||
- monospace accents
|
||||
- command lines
|
||||
- terminal windows
|
||||
- prompt bars
|
||||
- subtle grid
|
||||
- cyan, blue, coral, or lime accents
|
||||
- pixel or CRT texture if appropriate
|
||||
|
||||
Logo logic:
|
||||
- cursor + frame
|
||||
- bolt + build speed
|
||||
- scaffold + monogram
|
||||
- terminal glyph + symbol
|
||||
- modular construction mark
|
||||
|
||||
Mood:
|
||||
precise, sharp, confident, builder-native.
|
||||
|
||||
---
|
||||
|
||||
## Dark Product / Operator
|
||||
|
||||
Use for:
|
||||
business tools, growth tools, sales agents, automation, productivity.
|
||||
|
||||
Visual cues:
|
||||
- black / dark red / amber
|
||||
- glowing UI chips
|
||||
- card systems
|
||||
- segmented flows
|
||||
- icon rows
|
||||
- reward/progress motifs
|
||||
- minimal hero text
|
||||
|
||||
Logo logic:
|
||||
- signal, gift, path, operator mark, switch, loop, command system
|
||||
|
||||
Mood:
|
||||
fast, operational, tactical, premium.
|
||||
|
||||
---
|
||||
|
||||
## Dark Nature / Calm System
|
||||
|
||||
Use for:
|
||||
strategy, travel, wellness, climate, quiet premium SaaS.
|
||||
|
||||
Visual cues:
|
||||
- deep green
|
||||
- lime accent
|
||||
- misty landscapes
|
||||
- image UI circles
|
||||
- soft overlays
|
||||
- calm page labels
|
||||
- dark editorial grid
|
||||
|
||||
Logo logic:
|
||||
- path, leaf, moon, horizon, compass, portal, folded mark
|
||||
|
||||
Mood:
|
||||
calm, trustworthy, focused.
|
||||
|
||||
---
|
||||
|
||||
## Dark Security / Threat Intelligence
|
||||
|
||||
Use for:
|
||||
security, compliance, monitoring, network products.
|
||||
|
||||
Visual cues:
|
||||
- black/navy
|
||||
- shield forms
|
||||
- radar lines
|
||||
- threat labels
|
||||
- subtle motion traces
|
||||
- red/blue alert chips
|
||||
- controlled gradients
|
||||
|
||||
Logo logic:
|
||||
- shield, raptor, eye, watch, boundary, protected core
|
||||
|
||||
Mood:
|
||||
serious, vigilant, precise.
|
||||
|
||||
---
|
||||
|
||||
## Light Editorial / Compliance
|
||||
|
||||
Use for:
|
||||
legal, privacy, compliance, documents, trust brands.
|
||||
|
||||
Visual cues:
|
||||
- warm ivory
|
||||
- paper texture
|
||||
- small serif labels
|
||||
- seals / badges
|
||||
- color wheel / palette object
|
||||
- calm stationery
|
||||
- deep blue, red, gold accents
|
||||
|
||||
Logo logic:
|
||||
- seal, dog, shield, document, stamp, monogram
|
||||
|
||||
Mood:
|
||||
trustworthy, refined, institutional but modern.
|
||||
|
||||
---
|
||||
|
||||
## Luxury / Beauty / Fashion
|
||||
|
||||
Use for:
|
||||
beauty, fashion, hospitality, premium services.
|
||||
|
||||
Visual cues:
|
||||
- ivory / stone / espresso
|
||||
- serif wordmark
|
||||
- elegant monogram
|
||||
- paper grain
|
||||
- embossing
|
||||
- product labels
|
||||
- editorial crops
|
||||
- soft shadows
|
||||
|
||||
Logo logic:
|
||||
- monogram, seal, petal, vessel, ritual object, refined typographic mark
|
||||
|
||||
Mood:
|
||||
tasteful, adult, expensive.
|
||||
|
||||
---
|
||||
|
||||
## Voice / Communication
|
||||
|
||||
Use for:
|
||||
voice AI, chat, assistants, speech, audio.
|
||||
|
||||
Visual cues:
|
||||
- dark indigo
|
||||
- lilac glow
|
||||
- waveform
|
||||
- mic motif
|
||||
- phone crop
|
||||
- command input
|
||||
- app icon
|
||||
|
||||
Logo logic:
|
||||
- wave + initial
|
||||
- sound orb
|
||||
- speech path
|
||||
- microphone abstraction
|
||||
- pulse ring
|
||||
|
||||
Mood:
|
||||
fluid, intelligent, intimate.
|
||||
|
||||
---
|
||||
|
||||
## Cultural / Experimental
|
||||
|
||||
Use for:
|
||||
music, creative tools, events, gaming-adjacent, cultural products.
|
||||
|
||||
Visual cues:
|
||||
- halftone
|
||||
- CRT texture
|
||||
- analog print
|
||||
- bold accent color
|
||||
- poster-style panels
|
||||
- unexpected image crops
|
||||
- simple but punchy logo
|
||||
|
||||
Logo logic:
|
||||
- custom wordmark
|
||||
- icon with attitude
|
||||
- symbolic mascot
|
||||
- print-inspired mark
|
||||
|
||||
Mood:
|
||||
memorable, creative, still controlled.
|
||||
|
||||
---
|
||||
|
||||
# PREMIUM DETAIL LANGUAGE
|
||||
|
||||
Use details like:
|
||||
- small page numbers
|
||||
- tiny footer labels
|
||||
- precise alignment marks
|
||||
- construction lines
|
||||
- subtle crosshair grids
|
||||
- thin rules
|
||||
- browser bars
|
||||
- rounded rectangles
|
||||
- image masks
|
||||
- soft shadows
|
||||
- low-opacity texture
|
||||
- halftone image treatment
|
||||
- one highlighted word
|
||||
- one accent chip
|
||||
- one strong icon state
|
||||
|
||||
Do not overuse them.
|
||||
|
||||
Premium detail should reward looking closer.
|
||||
|
||||
---
|
||||
|
||||
# TEXT RULES
|
||||
|
||||
Use very little text.
|
||||
|
||||
Good text:
|
||||
- brand name
|
||||
- one tagline
|
||||
- one URL
|
||||
- one command
|
||||
- 2–5 section labels
|
||||
- short UI chips
|
||||
|
||||
Bad text:
|
||||
- long paragraphs
|
||||
- tiny fake body copy
|
||||
- lots of menu items
|
||||
- lorem ipsum
|
||||
- dense explanations
|
||||
- unreadable labels
|
||||
|
||||
Text should be large enough and sparse enough to render well.
|
||||
|
||||
---
|
||||
|
||||
# TAGLINE STYLE
|
||||
|
||||
Taglines should be short and specific.
|
||||
|
||||
Good:
|
||||
- "What will you build today?"
|
||||
- "Nothing random."
|
||||
- "Your network. Our watch."
|
||||
- "Build better."
|
||||
- "On guard."
|
||||
- "Every mission under control."
|
||||
- "Everything operators need."
|
||||
- "Clarity builds confidence."
|
||||
|
||||
Avoid:
|
||||
- generic corporate slogans
|
||||
- long marketing copy
|
||||
- buzzword soup
|
||||
- fake inspirational fluff
|
||||
|
||||
---
|
||||
|
||||
# IMAGE DIRECTION
|
||||
|
||||
Images should feel art-directed.
|
||||
|
||||
Use:
|
||||
- cinematic mountains
|
||||
- dusk skies
|
||||
- landscapes with brand overlays
|
||||
- halftone clouds
|
||||
- CRT screen scenes
|
||||
- dark product closeups
|
||||
- dramatic object crops
|
||||
- textured paper backgrounds
|
||||
- moody architecture
|
||||
- abstract but controlled visual systems
|
||||
|
||||
Avoid:
|
||||
- generic stock people
|
||||
- random office photos
|
||||
- cliché robot imagery
|
||||
- overbusy scenes
|
||||
- unrelated imagery
|
||||
|
||||
Images should match the palette and metaphor.
|
||||
|
||||
---
|
||||
|
||||
# MOCKUP DIRECTION
|
||||
|
||||
Mockups should be minimal and believable.
|
||||
|
||||
Use:
|
||||
- browser chrome
|
||||
- URL bar
|
||||
- terminal window
|
||||
- command prompt
|
||||
- app icon
|
||||
- phone corner crop
|
||||
- card stack
|
||||
- badge
|
||||
- seal
|
||||
- folder
|
||||
- UI chips
|
||||
- dashboard fragment
|
||||
- input bar
|
||||
- product label
|
||||
|
||||
Avoid:
|
||||
- full fake dashboards with too much data
|
||||
- cheap glossy mockups
|
||||
- random device overload
|
||||
- busy app screens
|
||||
- excessive icons
|
||||
|
||||
Mockups are identity applications, not feature demos.
|
||||
|
||||
---
|
||||
|
||||
# COLOR DISCIPLINE
|
||||
|
||||
Use one dominant palette.
|
||||
|
||||
Default:
|
||||
- base color
|
||||
- primary accent
|
||||
- secondary accent
|
||||
- neutrals
|
||||
|
||||
Good reference-style palettes:
|
||||
- black + cyan + muted coral
|
||||
- black + red + cream + blue
|
||||
- forest green + lime + fog gray
|
||||
- navy + white + steel
|
||||
- ivory + deep blue + red + gold
|
||||
- black + lilac + soft purple
|
||||
- black + amber + red
|
||||
- charcoal + white + pale blue
|
||||
|
||||
Rules:
|
||||
- accents must repeat across panels
|
||||
- no random rainbow unless requested
|
||||
- no generic purple-blue AI glow unless appropriate
|
||||
- one accent can carry the entire system
|
||||
|
||||
---
|
||||
|
||||
# ANTI-GENERIC RULES
|
||||
|
||||
Never make:
|
||||
- random floating icons
|
||||
- generic startup gradients
|
||||
- overdesigned logos
|
||||
- meaningless blobs
|
||||
- messy layout collages
|
||||
- fake tiny UI
|
||||
- inconsistent logo marks
|
||||
- too many colors
|
||||
- cheap neon
|
||||
- stock-template brand boards
|
||||
- corporate PowerPoint slides
|
||||
- soulless SaaS dashboards
|
||||
|
||||
Make the design quieter, sharper, and more intentional.
|
||||
|
||||
---
|
||||
|
||||
# REFERENCE USAGE
|
||||
|
||||
When the user provides references:
|
||||
|
||||
Extract:
|
||||
- layout rhythm
|
||||
- grid style
|
||||
- spacing
|
||||
- typography scale
|
||||
- visual density
|
||||
- logo placement
|
||||
- amount of text
|
||||
- image treatment
|
||||
- accent color logic
|
||||
- brand-system behavior
|
||||
|
||||
Do not copy:
|
||||
- exact logo
|
||||
- exact brand name
|
||||
- exact composition
|
||||
- exact slogan
|
||||
- unique visual asset
|
||||
|
||||
Use references as quality training, not as templates.
|
||||
|
||||
---
|
||||
|
||||
# PROMPT TEMPLATE
|
||||
|
||||
Use this structure internally:
|
||||
|
||||
Create a premium brand-kit overview image for "[BRAND NAME]".
|
||||
|
||||
Brand strategy:
|
||||
- category: [category]
|
||||
- audience: [audience]
|
||||
- personality: [traits]
|
||||
- core metaphor: [metaphor]
|
||||
- logo idea: [how the mark combines symbol + name + category meaning]
|
||||
|
||||
Layout:
|
||||
[3×3 / 2×3 / custom] grid on a dark or light presentation canvas with strong gutters, clean alignment, and refined negative space.
|
||||
|
||||
Panels:
|
||||
- logo cover
|
||||
- logo concept / construction
|
||||
- digital application
|
||||
- tagline / brand essence
|
||||
- color system
|
||||
- typography
|
||||
- physical application
|
||||
- image direction
|
||||
- system detail
|
||||
|
||||
Visual mode:
|
||||
[mode]
|
||||
|
||||
Palette:
|
||||
[disciplined palette]
|
||||
|
||||
Style:
|
||||
premium, sparse, cinematic, intentional, polished, brand-guidelines deck, no clutter, no copied real-world logos.
|
||||
|
||||
Typography:
|
||||
readable, minimal, high hierarchy, no tiny fake text.
|
||||
|
||||
Logo:
|
||||
professional, symbolic, simple, ownable, based on the brand's purpose, repeated consistently across panels.
|
||||
|
||||
---
|
||||
|
||||
# FINAL OUTPUT STANDARD
|
||||
|
||||
The image must look like:
|
||||
- a premium identity deck
|
||||
- a senior designer's presentation board
|
||||
- a brand-system case study
|
||||
- a visual launch direction
|
||||
- a professional logo concept board
|
||||
|
||||
The final result should be:
|
||||
- clean
|
||||
- strategic
|
||||
- symbolic
|
||||
- minimal
|
||||
- coherent
|
||||
- premium
|
||||
- art-directed
|
||||
- implementation-friendly
|
||||
- stronger than normal AI-generated brand visuals
|
||||
File diff suppressed because it is too large
Load Diff
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
42
.agents/skills/frontend-design/SKILL.md
Normal file
42
.agents/skills/frontend-design/SKILL.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: high-end-visual-design
|
||||
description: Teaches the AI to design like a high-end agency. Defines the exact fonts, spacing, shadows, card structures, and animations that make a website feel expensive. Blocks all the common defaults that make AI designs look cheap or generic.
|
||||
---
|
||||
|
||||
# Agent Skill: Principal UI/UX Architect & Motion Choreographer (Awwwards-Tier)
|
||||
|
||||
## 1. Meta Information & Core Directive
|
||||
- **Persona:** `Vanguard_UI_Architect`
|
||||
- **Objective:** You engineer $150k+ agency-level digital experiences, not just websites. Your output must exude haptic depth, cinematic spatial rhythm, obsessive micro-interactions, and flawless fluid motion.
|
||||
- **The Variance Mandate:** NEVER generate the exact same layout or aesthetic twice in a row. You must dynamically combine different premium layout archetypes and texture profiles while strictly adhering to the elite "Apple-esque / Linear-tier" design language.
|
||||
|
||||
## 2. THE "ABSOLUTE ZERO" DIRECTIVE (STRICT ANTI-PATTERNS)
|
||||
If your generated code includes ANY of the following, the design instantly fails:
|
||||
- **Banned Fonts:** Inter, Roboto, Arial, Open Sans, Helvetica. (Assume premium fonts like `Geist`, `Clash Display`, `PP Editorial New`, or `Plus Jakarta Sans` are available).
|
||||
- **Banned Icons:** Standard thick-stroked Lucide, FontAwesome, or Material Icons. Use only ultra-light, precise lines (e.g., Phosphor Light, Remix Line).
|
||||
- **Banned Borders & Shadows:** Generic 1px solid gray borders. Harsh, dark drop shadows (`shadow-md`, `rgba(0,0,0,0.3)`).
|
||||
- **Banned Layouts:** Edge-to-edge sticky navbars glued to the top. Symmetrical, boring 3-column Bootstrap-style grids without massive whitespace gaps.
|
||||
- **Banned Motion:** Standard `linear` or `ease-in-out` transitions. Instant state changes without interpolation.
|
||||
|
||||
## 3. THE CREATIVE VARIANCE ENGINE
|
||||
Before writing code, silently "roll the dice" and select ONE combination from the following archetypes based on the prompt's context to ensure the output is uniquely tailored but always premium:
|
||||
|
||||
### A. Vibe & Texture Archetypes (Pick 1)
|
||||
1. **Ethereal Glass (SaaS / AI / Tech):** Deepest OLED black (`#050505`), radial mesh gradients (e.g., subtle glowing purple/emerald orbs) in the background. Vantablack cards with heavy `backdrop-blur-2xl` and pure white/10 hairlines. Wide geometric Grotesk typography.
|
||||
2. **Editorial Luxury (Lifestyle / Real Estate / Agency):** Warm creams (`#FDFBF7`), muted sage, or deep espresso tones. High-contrast Variable Serif fonts for massive headings. Subtle CSS noise/film-grain overlay (`opacity-[0.03]`) for a physical paper feel.
|
||||
3. **Soft Structuralism (Consumer / Health / Portfolio):** Silver-grey or completely white backgrounds. Massive bold Grotesk typography. Airy, floating components with unbelievably soft, highly diffused ambient shadows.
|
||||
|
||||
### B. Layout Archetypes (Pick 1)
|
||||
1. **The Asymmetrical Bento:** A masonry-like CSS Grid of varying card sizes (e.g., `col-span-8 row-span-2` next to stacked `col-span-4` cards) to break visual monotony.
|
||||
- **Mobile Collapse:** Falls back to a single-column stack (`grid-cols-1`) with generous vertical gaps (`gap-6`). All `col-span` overrides reset to `col-span-1`.
|
||||
2. **The Z-Axis Cascade:** Elements are stacked like physical cards, slightly overlapping each other with varying depths of field, some with a subtle `-2deg` or `3deg` rotation to break the digital grid.
|
||||
- **Mobile Collapse:** Remove all rotations and negative-margin overlaps below `768px`. Stack vertically with standard spacing. Overlapping elements cause touch-target conflicts on mobile.
|
||||
3. **The Editorial Split:** Massive typography on the left half (`w-1/2`), with interactive, scrollable horizontal image pills or staggered interactive cards on the right.
|
||||
- **Mobile Collapse:** Converts to a full-width vertical stack (`w-full`). Typography block sits on top, interactive content flows below with horizontal scroll preserved if needed.
|
||||
|
||||
**Mobile Override (Universal):** Any asymmetric layout above `md:` MUST aggressively fall back to `w-full`, `px-4`, `py-8` on viewports below `768px`. Never use `h-screen` for full-height sections — always use `min-h-[100dvh]` to prevent iOS Safari viewport jumping.
|
||||
|
||||
## 4. HAPTIC MICRO-AESTHETICS (COMPONENT MASTERY)
|
||||
|
||||
### A. The "Double-Bezel" (Doppelrand / Nested Architecture)
|
||||
Never place a premium card, image, or container flatly on the background. They must look like physical, machined hardware (like a glass plate sitting in an aluminum tray) using nested enclosures.
|
||||
- **Outer Shell:** A wrapper `div` with a subtle background (`bg-black/5` or `bg-white/5`), a hairline outer border (`ring-1 ring-black/5` or `border border-white/10`), a specific padding (e.g., `p-1.5` or `p-2`), and a large outer radius (`rounded-[2rem]`).
|
||||
- **Inner Core:** The actual content container inside the shell. It has its own distinct background color, its own inner highlight (`shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)]`), and a mathematically calculated smaller radius (e.g., `rounded-[calc(2rem-0.375rem)]`) for concentric curves.
|
||||
|
||||
### B. Nested CTA & "Island" Button Architecture
|
||||
- **Structure:** Primary interactive buttons must be fully rounded pills (`rounded-full`) with generous padding (`px-6 py-3`).
|
||||
- **The "Button-in-Button" Trailing Icon:** If a button has an arrow (`↗`), it NEVER sits naked next to the text. It must be nested inside its own distinct circular wrapper (e.g., `w-8 h-8 rounded-full bg-black/5 dark:bg-white/10 flex items-center justify-center`) placed completely flush with the main button's right inner padding.
|
||||
|
||||
### C. Spatial Rhythm & Tension
|
||||
- **Macro-Whitespace:** Double your standard padding. Use `py-24` to `py-40` for sections. Allow the design to breathe heavily.
|
||||
- **Eyebrow Tags:** Precede major H1/H2s with a microscopic, pill-shaped badge (`rounded-full px-3 py-1 text-[10px] uppercase tracking-[0.2em] font-medium`).
|
||||
|
||||
## 5. MOTION CHOREOGRAPHY (FLUID DYNAMICS)
|
||||
Never use default transitions. All motion must simulate real-world mass and spring physics. Use custom cubic-beziers (e.g., `transition-all duration-700 ease-[cubic-bezier(0.32,0.72,0,1)]`).
|
||||
|
||||
### A. The "Fluid Island" Nav & Hamburger Reveal
|
||||
- **Closed State:** The Navbar is a floating glass pill detached from the top (`mt-6`, `mx-auto`, `w-max`, `rounded-full`).
|
||||
- **The Hamburger Morph:** On click, the 2 or 3 lines of the hamburger icon must fluidly rotate and translate to form a perfect 'X' (`rotate-45` and `-rotate-45` with absolute positioning), not just disappear.
|
||||
- **The Modal Expansion:** The menu should open as a massive, screen-filling overlay with a heavy glass effect (`backdrop-blur-3xl bg-black/80` or `bg-white/80`).
|
||||
- **Staggered Mask Reveal:** The navigation links inside the expanded state do not just appear. They fade in and slide up from an invisible box (`translate-y-12 opacity-0` to `translate-y-0 opacity-100`) with a staggered delay (`delay-100`, `delay-150`, `delay-200` for each item).
|
||||
|
||||
### B. Magnetic Button Hover Physics
|
||||
- Use the `group` utility. On hover, do not just change the background color.
|
||||
- Scale the entire button down slightly (`active:scale-[0.98]`) to simulate physical pressing.
|
||||
- The nested inner icon circle should translate diagonally (`group-hover:translate-x-1 group-hover:-translate-y-[1px]`) and scale up slightly (`scale-105`), creating internal kinetic tension.
|
||||
|
||||
### C. Scroll Interpolation (Entry Animations)
|
||||
- Elements never appear statically on load. As they enter the viewport, they must execute a gentle, heavy fade-up (`translate-y-16 blur-md opacity-0` resolving to `translate-y-0 blur-0 opacity-100` over 800ms+).
|
||||
- For JavaScript-driven scroll reveals, use `IntersectionObserver` or Framer Motion's `whileInView`. Never use `window.addEventListener('scroll')` — it causes continuous reflows and kills mobile performance.
|
||||
|
||||
## 6. PERFORMANCE GUARDRAILS
|
||||
- **GPU-Safe Animation:** Never animate `top`, `left`, `width`, or `height`. Animate exclusively via `transform` and `opacity`. Use `will-change: transform` sparingly and only on elements that are actively animating.
|
||||
- **Blur Constraints:** Apply `backdrop-blur` only to fixed or sticky elements (navbars, overlays). Never apply blur filters to scrolling containers or large content areas — this causes continuous GPU repaints and severe mobile frame drops.
|
||||
- **Grain/Noise Overlays:** Apply noise textures exclusively to fixed, `pointer-events-none` pseudo-elements (`position: fixed; inset: 0; z-index: 50`). Never attach them to scrolling containers.
|
||||
- **Z-Index Discipline:** Do not use arbitrary `z-50` or `z-[9999]`. Reserve z-indexes strictly for systemic layers: sticky nav, modals, overlays, tooltips.
|
||||
|
||||
## 7. EXECUTION PROTOCOL
|
||||
When generating UI code, follow this exact sequence:
|
||||
1. **[SILENT THOUGHT]** Roll the Variance Engine (Section 3). Choose your Vibe and Layout Archetypes based on the prompt's context to ensure a unique output.
|
||||
2. **[SCAFFOLD]** Establish the background texture, macro-whitespace scale, and massive typography sizes.
|
||||
3. **[ARCHITECT]** Build the DOM strictly using the "Double-Bezel" (Doppelrand) technique for all major cards, inputs, and feature grids. Use exaggerated squircle radii (`rounded-[2rem]`).
|
||||
4. **[CHOREOGRAPH]** Inject the custom `cubic-bezier` transitions, the staggered navigation reveals, and the button-in-button hover physics.
|
||||
5. **[OUTPUT]** Deliver flawless, pixel-perfect React/Tailwind/HTML code. Do not include basic, generic fallbacks.
|
||||
|
||||
## 8. PRE-OUTPUT CHECKLIST
|
||||
Evaluate your code against this matrix before delivering. This is the last filter.
|
||||
- [ ] No banned fonts, icons, borders, shadows, layouts, or motion patterns from Section 2 are present
|
||||
- [ ] A Vibe Archetype and Layout Archetype from Section 3 were consciously selected and applied
|
||||
- [ ] All major cards and containers use the Double-Bezel nested architecture (outer shell + inner core)
|
||||
- [ ] CTA buttons use the Button-in-Button trailing icon pattern where applicable
|
||||
- [ ] Section padding is at minimum `py-24` — the layout breathes heavily
|
||||
- [ ] All transitions use custom cubic-bezier curves — no `linear` or `ease-in-out`
|
||||
- [ ] Scroll entry animations are present — no element appears statically
|
||||
- [ ] Layout collapses gracefully below `768px` to single-column with `w-full` and `px-4`
|
||||
- [ ] All animations use only `transform` and `opacity` — no layout-triggering properties
|
||||
- [ ] `backdrop-blur` is only applied to fixed/sticky elements, never to scrolling content
|
||||
- [ ] The overall impression reads as "$150k agency build", not "template with nice fonts"
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
name: industrial-brutalist-ui
|
||||
description: Raw mechanical interfaces fusing Swiss typographic print with military terminal aesthetics. Rigid grids, extreme type scale contrast, utilitarian color, analog degradation effects. For data-heavy dashboards, portfolios, or editorial sites that need to feel like declassified blueprints.
|
||||
---
|
||||
|
||||
# SKILL: Industrial Brutalism & Tactical Telemetry UI
|
||||
|
||||
## 1. Skill Meta
|
||||
**Name:** Industrial Brutalism & Tactical Telemetry Interface Engineering
|
||||
**Description:** Advanced proficiency in architecting web interfaces that synthesize mid-century Swiss Typographic design, industrial manufacturing manuals, and retro-futuristic aerospace/military terminal interfaces. This discipline requires absolute mastery over rigid modular grids, extreme typographic scale contrast, purely utilitarian color palettes, and the programmatic simulation of analog degradation (halftones, CRT scanlines, bitmap dithering). The objective is to construct digital environments that project raw functionality, mechanical precision, and high data density, deliberately discarding conventional consumer UI patterns.
|
||||
|
||||
## 2. Visual Archetypes
|
||||
The design system operates by merging two distinct but highly compatible visual paradigms. **Pick ONE per project and commit to it. Do not alternate or mix both modes within the same interface.**
|
||||
|
||||
### 2.1 Swiss Industrial Print
|
||||
Derived from 1960s corporate identity systems and heavy machinery blueprints.
|
||||
* **Characteristics:** High-contrast light modes (newsprint/off-white substrates). Reliance on monolithic, heavy sans-serif typography. Unforgiving structural grids outlined by visible dividing lines. Aggressive, asymmetric use of negative space punctuated by oversized, viewport-bleeding numerals or letterforms. Heavy use of primary red as an alert/accent color.
|
||||
|
||||
### 2.2 Tactical Telemetry & CRT Terminal
|
||||
Derived from classified military databases, legacy mainframes, and aerospace Heads-Up Displays (HUDs).
|
||||
* **Characteristics:** Dark mode exclusivity. High-density tabular data presentation. Absolute dominance of monospaced typography. Integration of technical framing devices (ASCII brackets, crosshairs). Application of simulated hardware limitations (phosphor glow, scanlines, low bit-depth rendering).
|
||||
|
||||
## 3. Typographic Architecture
|
||||
Typography is the primary structural and decorative infrastructure. Imagery is secondary. The system demands extreme variance in scale, weight, and spacing.
|
||||
|
||||
### 3.1 Macro-Typography (Structural Headers)
|
||||
* **Classification:** Neo-Grotesque / Heavy Sans-Serif.
|
||||
* **Optimal Web Fonts:** Neue Haas Grotesk (Black), Inter (Extra Bold/Black), Archivo Black, Roboto Flex (Heavy), Monument Extended.
|
||||
* **Implementation Parameters:**
|
||||
* **Scale:** Deployed at massive scales using fluid typography (e.g., `clamp(4rem, 10vw, 15rem)`).
|
||||
* **Tracking (Letter-spacing):** Extremely tight, often negative (`-0.03em` to `-0.06em`), forcing glyphs to form solid architectural blocks.
|
||||
* **Leading (Line-height):** Highly compressed (`0.85` to `0.95`).
|
||||
* **Casing:** Exclusively uppercase for structural impact.
|
||||
|
||||
### 3.2 Micro-Typography (Data & Telemetry)
|
||||
* **Classification:** Monospace / Technical Sans.
|
||||
* **Optimal Web Fonts:** JetBrains Mono, IBM Plex Mono, Space Mono, VT323, Courier Prime.
|
||||
* **Implementation Parameters:**
|
||||
* **Scale:** Fixed and small (`10px` to `14px` / `0.7rem` to `0.875rem`).
|
||||
* **Tracking:** Generous (`0.05em` to `0.1em`) to simulate mechanical typewriter spacing or terminal matrices.
|
||||
* **Leading:** Standard to tight (`1.2` to `1.4`).
|
||||
* **Casing:** Exclusively uppercase. Used for all metadata, navigation, unit IDs, and coordinates.
|
||||
|
||||
### 3.3 Textural Contrast (Artistic Disruption)
|
||||
* **Classification:** High-Contrast Serif.
|
||||
* **Optimal Web Fonts:** Playfair Display, EB Garamond, Times New Roman.
|
||||
* **Implementation Parameters:** Used exceedingly sparingly. Must be subjected to heavy post-processing (halftone filters, 1-bit dithering) to degrade vector perfection and create textural juxtaposition against the clean sans-serifs.
|
||||
|
||||
## 4. Color System
|
||||
The color architecture is uncompromising. Gradients, soft drop shadows, and modern translucency are strictly prohibited. Colors simulate physical media or primitive emissive displays.
|
||||
|
||||
**CRITICAL: Choose ONE substrate palette per project and use it consistently. Never mix light and dark substrates within the same interface.**
|
||||
|
||||
### If Swiss Industrial Print (Light):
|
||||
* **Background:** `#F4F4F0` or `#EAE8E3` (Matte, unbleached documentation paper).
|
||||
* **Foreground:** `#050505` to `#111111` (Carbon Ink).
|
||||
* **Accent:** `#E61919` or `#FF2A2A` (Aviation/Hazard Red). This is the ONLY accent color. Used for strike-throughs, thick structural dividing lines, or vital data highlights.
|
||||
|
||||
### If Tactical Telemetry (Dark):
|
||||
* **Background:** `#0A0A0A` or `#121212` (Deactivated CRT. Avoid pure `#000000`).
|
||||
* **Foreground:** `#EAEAEA` (White phosphor). This is the primary text color.
|
||||
* **Accent:** `#E61919` or `#FF2A2A` (Aviation/Hazard Red). Same red, same rules.
|
||||
* **Terminal Green (`#4AF626`):** Optional. Use ONLY for a single specific UI element (e.g., one status indicator or one data readout) — never as a general text color. If it doesn't serve a clear purpose, omit it entirely.
|
||||
|
||||
## 5. Layout and Spatial Engineering
|
||||
The layout must appear mathematically engineered. It rejects conventional web padding in favor of visible compartmentalization.
|
||||
|
||||
* **The Blueprint Grid:** Strict adherence to CSS Grid architectures. Elements do not float; they are anchored precisely to grid tracks and intersections.
|
||||
* **Visible Compartmentalization:** Extensive utilization of solid borders (`1px` or `2px solid`) to delineate distinct zones of information. Horizontal rules (`<hr>`) frequently span the entire container width to segregate operational units.
|
||||
* **Bimodal Density:** Layouts oscillate between extreme data density (tightly packed monospace metadata clustered together) and vast expanses of calculated negative space framing macro-typography.
|
||||
* **Geometry:** Absolute rejection of `border-radius`. All corners must be exactly 90 degrees to enforce mechanical rigidity.
|
||||
|
||||
## 6. UI Components and Symbology
|
||||
Standard web UI conventions are replaced with utilitarian, industrial graphic elements.
|
||||
|
||||
* **Syntax Decoration:** Utilization of ASCII characters to frame data points.
|
||||
* *Framing:* `[ DELIVERY SYSTEMS ]`, `< RE-IND >`
|
||||
* *Directional:* `>>>`, `///`, `\\\\`
|
||||
* **Industrial Markers:** Prominent integration of registration (`®`), copyright (`©`), and trademark (`™`) symbols functioning as structural geometric elements rather than legal text.
|
||||
* **Technical Assets:** Integration of crosshairs (`+`) at grid intersections, repeating vertical lines (barcodes), thick horizontal warning stripes, and randomized string data (e.g., `REV 2.6`, `UNIT / D-01`) to simulate active mechanical processes.
|
||||
|
||||
## 7. Textural and Post-Processing Effects
|
||||
To prevent the design from appearing purely digital, simulated analog degradation is engineered into the frontend via CSS and SVG filters.
|
||||
|
||||
* **Halftone and 1-Bit Dithering:** Transforming continuous-tone images or large serif typography into dot-matrix patterns. Achieved via pre-processing or CSS `mix-blend-mode: multiply` overlays combined with SVG radial dot patterns.
|
||||
* **CRT Scanlines:** For terminal interfaces, applying a `repeating-linear-gradient` to the background to simulate horizontal electron beam sweeps (e.g., `repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px)`).
|
||||
* **Mechanical Noise:** A global, low-opacity SVG static/noise filter applied to the DOM root to introduce a unified physical grain across both dark and light modes.
|
||||
|
||||
## 8. Web Engineering Directives
|
||||
1. **Grid Determinism:** Utilize `display: grid; gap: 1px;` with contrasting parent/child background colors to generate mathematically perfect, razor-thin dividing lines without complex border declarations.
|
||||
2. **Semantic Rigidity:** Construct the DOM using precise semantic tags (`<data>`, `<samp>`, `<kbd>`, `<output>`, `<dl>`) to accurately reflect the technical nature of the telemetry.
|
||||
3. **Typography Clamping:** Implement CSS `clamp()` functions exclusively for macro-typography to ensure massive text scales aggressively while maintaining structural integrity across viewports.
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
name: minimalist-ui
|
||||
description: Clean editorial-style interfaces. Warm monochrome palette, typographic contrast, flat bento grids, muted pastels. No gradients, no heavy shadows.
|
||||
---
|
||||
|
||||
# Protocol: Premium Utilitarian Minimalism UI Architect
|
||||
|
||||
## 1. Protocol Overview
|
||||
Name: Premium Utilitarian Minimalism & Editorial UI
|
||||
Description: An advanced frontend engineering directive for generating highly refined, ultra-minimalist, "document-style" web interfaces analogous to top-tier workspace platforms. This protocol strictly enforces a high-contrast warm monochrome palette, bespoke typographic hierarchies, meticulous structural macro-whitespace, bento-grid layouts, and an ultra-flat component architecture with deliberate muted pastel accents. It actively rejects standard generic SaaS design trends.
|
||||
|
||||
## 2. Absolute Negative Constraints (Banned Elements)
|
||||
The AI must strictly avoid the following generic web development defaults:
|
||||
- DO NOT use the "Inter", "Roboto", or "Open Sans" typefaces.
|
||||
- DO NOT use generic, thin-line icon libraries like "Lucide", "Feather", or standard "Heroicons".
|
||||
- DO NOT use Tailwind's default heavy drop shadows (e.g., `shadow-md`, `shadow-lg`, `shadow-xl`). Shadows must be practically non-existent or heavily customized to be ultra-diffuse and low opacity (< 0.05).
|
||||
- DO NOT use primary colored backgrounds for large elements or sections (e.g., no bright blue, green, or red hero sections).
|
||||
- DO NOT use gradients, neon colors, or 3D glassmorphism (beyond subtle navbar blurs).
|
||||
- DO NOT use `rounded-full` (pill shapes) for large containers, cards, or primary buttons.
|
||||
- DO NOT use emojis anywhere in code, markup, text content, headings, or alt text. Replace with proper icons or clean SVG primitives.
|
||||
- DO NOT use generic placeholder names like "John Doe", "Acme Corp", or "Lorem Ipsum". Use realistic, contextual content.
|
||||
- DO NOT use AI copywriting clichés: "Elevate", "Seamless", "Unleash", "Next-Gen", "Game-changer", "Delve". Write plain, specific language.
|
||||
|
||||
## 3. Typographic Architecture
|
||||
The interface must rely on extreme typographic contrast and premium font selection to establish an editorial feel.
|
||||
- Primary Sans-Serif (Body, UI, Buttons): Use clean, geometric, or system-native fonts with character. Target: `font-family: 'SF Pro Display', 'Geist Sans', 'Helvetica Neue', 'Switzer', sans-serif`.
|
||||
- Editorial Serif (Hero Headings & Quotes): Target: `font-family: 'Lyon Text', 'Newsreader', 'Playfair Display', 'Instrument Serif', serif`. Apply tight tracking (`letter-spacing: -0.02em` to `-0.04em`) and tight line-height (`1.1`).
|
||||
- Monospace (Code, Keystrokes, Meta-data): Target: `font-family: 'Geist Mono', 'SF Mono', 'JetBrains Mono', monospace`.
|
||||
- Text Colors: Body text must never be absolute black (`#000000`). Use off-black/charcoal (`#111111` or `#2F3437`) with a generous `line-height` of `1.6` for legibility. Secondary text should be muted gray (`#787774`).
|
||||
|
||||
## 4. Color Palette (Warm Monochrome + Spot Pastels)
|
||||
Color is a scarce resource, utilized only for semantic meaning or subtle accents.
|
||||
- Canvas / Background: Pure White `#FFFFFF` or Warm Bone/Off-White `#F7F6F3` / `#FBFBFA`.
|
||||
- Primary Surface (Cards): `#FFFFFF` or `#F9F9F8`.
|
||||
- Structural Borders / Dividers: Ultra-light gray `#EAEAEA` or `rgba(0,0,0,0.06)`.
|
||||
- Accent Colors: Exclusively use highly desaturated, washed-out pastels for tags, inline code backgrounds, or subtle icon backgrounds.
|
||||
- Pale Red: `#FDEBEC` (Text: `#9F2F2D`)
|
||||
- Pale Blue: `#E1F3FE` (Text: `#1F6C9F`)
|
||||
- Pale Green: `#EDF3EC` (Text: `#346538`)
|
||||
- Pale Yellow: `#FBF3DB` (Text: `#956400`)
|
||||
|
||||
## 5. Component Specifications
|
||||
- Bento Box Feature Grids:
|
||||
- Utilize asymmetrical CSS Grid layouts.
|
||||
- Cards must have exactly `border: 1px solid #EAEAEA`.
|
||||
- Border-radius must be crisp: `8px` or `12px` maximum.
|
||||
- Internal padding must be generous (e.g., `24px` to `40px`).
|
||||
- Primary Call-To-Action (Buttons):
|
||||
- Solid background `#111111`, text `#FFFFFF`.
|
||||
- Slight border-radius (`4px` to `6px`). No box-shadow.
|
||||
- Hover state should be a subtle color shift to `#333333` or a micro-scale `transform: scale(0.98)`.
|
||||
- Tags & Status Badges:
|
||||
- Pill-shaped (`border-radius: 9999px`), very small typography (`text-xs`), uppercase with wide tracking (`letter-spacing: 0.05em`).
|
||||
- Background must use the defined Muted Pastels.
|
||||
- Accordions (FAQ):
|
||||
- Strip all container boxes. Separate items only with a `border-bottom: 1px solid #EAEAEA`.
|
||||
- Use a clean, sharp `+` and `-` icon for the toggle state.
|
||||
- Keystroke Micro-UIs:
|
||||
- Render shortcuts as physical keys using `<kbd>` tags: `border: 1px solid #EAEAEA`, `border-radius: 4px`, `background: #F7F6F3`, using the Monospace font.
|
||||
- Faux-OS Window Chrome:
|
||||
- When mocking up software, wrap it in a minimalist container with a white top bar containing three small, light gray circles (replicating macOS window controls).
|
||||
|
||||
## 6. Iconography & Imagery Directives
|
||||
- System Icons: Use "Phosphor Icons (Bold or Fill weights)" or "Radix UI Icons" for a technical, slightly thicker-stroke aesthetic. Standardize stroke width across all icons.
|
||||
- Illustrations: Monochromatic, rough continuous-line ink sketches on a white background, featuring a single offset geometric shape filled with a muted pastel color.
|
||||
- Photography: Use high-quality, desaturated images with a warm tone. Apply subtle overlays (`opacity: 0.04` warm grain) to blend photos into the monochrome palette. Never use oversaturated stock photos. Use reliable placeholders like `https://picsum.photos/seed/{context}/1200/800` when real assets are unavailable.
|
||||
- Hero & Section Backgrounds: Sections should not feel empty and flat. Use subtle full-width background imagery at very low opacity, soft radial light spots (`radial-gradient` with warm tones at `opacity: 0.03`), or minimal geometric line patterns to add depth without breaking the clean aesthetic.
|
||||
|
||||
## 7. Subtle Motion & Micro-Animations
|
||||
Motion should feel invisible — present but never distracting. The goal is quiet sophistication, not spectacle.
|
||||
- Scroll Entry: Elements fade in gently as they enter the viewport. Use `translateY(12px)` + `opacity: 0` resolving over `600ms` with `cubic-bezier(0.16, 1, 0.3, 1)`. Use `IntersectionObserver`, never `window.addEventListener('scroll')`.
|
||||
- Hover States: Cards lift with an ultra-subtle shadow shift (`box-shadow` transitioning from `0 0 0` to `0 2px 8px rgba(0,0,0,0.04)` over `200ms`). Buttons respond with `scale(0.98)` on `:active`.
|
||||
- Staggered Reveals: Lists and grid items enter with a cascade delay (`animation-delay: calc(var(--index) * 80ms)`). Never mount everything at once.
|
||||
- Background Ambient Motion: Optional. A single, very slow-moving radial gradient blob (`animation-duration: 20s+`, `opacity: 0.02-0.04`) drifting behind hero sections. Must be applied to a `position: fixed; pointer-events: none` layer. Never on scrolling containers.
|
||||
- Performance: Animate exclusively via `transform` and `opacity`. No layout-triggering properties (`top`, `left`, `width`, `height`). Use `will-change: transform` sparingly and only on actively animating elements.
|
||||
|
||||
## 8. Execution Protocol
|
||||
When tasked with writing frontend code (HTML, React, Tailwind, Vue) or designing a layout:
|
||||
1. Establish the macro-whitespace first. Use massive vertical padding between sections (e.g., `py-24` or `py-32` in Tailwind).
|
||||
2. Constrain the main typography content width to `max-w-4xl` or `max-w-5xl`.
|
||||
3. Apply the custom typographic hierarchy and monochromatic color variables immediately.
|
||||
4. Ensure every card, divider, and border adheres strictly to the `1px solid #EAEAEA` rule.
|
||||
5. Add scroll-entry animations to all major content blocks.
|
||||
6. Ensure sections have visual depth through imagery, ambient gradients, or subtle textures — no empty flat backgrounds.
|
||||
7. Provide code that reflects this high-end, uncluttered, editorial aesthetic natively without requiring manual adjustments.
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
name: redesign-existing-projects
|
||||
description: Upgrades existing websites and apps to premium quality. Audits current design, identifies generic AI patterns, and applies high-end design standards without breaking functionality. Works with any CSS framework or vanilla CSS.
|
||||
---
|
||||
|
||||
# Redesign Skill
|
||||
|
||||
## How This Works
|
||||
|
||||
When applied to an existing project, follow this sequence:
|
||||
|
||||
1. **Scan** — Read the codebase. Identify the framework, styling method (Tailwind, vanilla CSS, styled-components, etc.), and current design patterns.
|
||||
2. **Diagnose** — Run through the audit below. List every generic pattern, weak point, and missing state you find.
|
||||
3. **Fix** — Apply targeted upgrades working with the existing stack. Do not rewrite from scratch. Improve what's there.
|
||||
|
||||
## Design Audit
|
||||
|
||||
### Typography
|
||||
|
||||
Check for these problems and fix them:
|
||||
|
||||
- **Browser default fonts or Inter everywhere.** Replace with a font that has character. Good options: `Geist`, `Outfit`, `Cabinet Grotesk`, `Satoshi`. For editorial/creative projects, pair a serif header with a sans-serif body.
|
||||
- **Headlines lack presence.** Increase size for display text, tighten letter-spacing, reduce line-height. Headlines should feel heavy and intentional.
|
||||
- **Body text too wide.** Limit paragraph width to roughly 65 characters. Increase line-height for readability.
|
||||
- **Only Regular (400) and Bold (700) weights used.** Introduce Medium (500) and SemiBold (600) for more subtle hierarchy.
|
||||
- **Numbers in proportional font.** Use a monospace font or enable tabular figures (`font-variant-numeric: tabular-nums`) for data-heavy interfaces.
|
||||
- **Missing letter-spacing adjustments.** Use negative tracking for large headers, positive tracking for small caps or labels.
|
||||
- **All-caps subheaders everywhere.** Try lowercase italics, sentence case, or small-caps instead.
|
||||
- **Orphaned words.** Single words sitting alone on the last line. Fix with `text-wrap: balance` or `text-wrap: pretty`.
|
||||
|
||||
### Color and Surfaces
|
||||
|
||||
- **Pure `#000000` background.** Replace with off-black, dark charcoal, or tinted dark (`#0a0a0a`, `#121212`, or a dark navy).
|
||||
- **Oversaturated accent colors.** Keep saturation below 80%. Desaturate accents so they blend with neutrals instead of screaming.
|
||||
- **More than one accent color.** Pick one. Remove the rest. Consistency beats variety.
|
||||
- **Mixing warm and cool grays.** Stick to one gray family. Tint all grays with a consistent hue (warm or cool, not both).
|
||||
- **Purple/blue "AI gradient" aesthetic.** This is the most common AI design fingerprint. Replace with neutral bases and a single, considered accent.
|
||||
- **Generic `box-shadow`.** Tint shadows to match the background hue. Use colored shadows (e.g., dark blue shadow on a blue background) instead of pure black at low opacity.
|
||||
- **Flat design with zero texture.** Add subtle noise, grain, or micro-patterns to backgrounds. Pure flat vectors feel sterile.
|
||||
- **Perfectly even gradients.** Break the uniformity with radial gradients, noise overlays, or mesh gradients instead of standard linear 45-degree fades.
|
||||
- **Inconsistent lighting direction.** Audit all shadows to ensure they suggest a single, consistent light source.
|
||||
- **Random dark sections in a light mode page (or vice versa).** A single dark-background section breaking an otherwise light page looks like a copy-paste accident. Either commit to a full dark mode or keep a consistent background tone throughout. If contrast is needed, use a slightly darker shade of the same palette — not a sudden jump to `#111` in the middle of a cream page.
|
||||
- **Empty, flat sections with no visual depth.** Sections that are just text on a plain background feel unfinished. Add high-quality background imagery (blurred, overlaid, or masked), subtle patterns, or ambient gradients. Use reliable placeholder sources like `https://picsum.photos/seed/{name}/1920/1080` when real assets are not available. Experiment with background images behind hero sections, feature blocks, or CTAs — even a subtle full-width photo at low opacity adds presence.
|
||||
|
||||
### Layout
|
||||
|
||||
- **Everything centered and symmetrical.** Break symmetry with offset margins, mixed aspect ratios, or left-aligned headers over centered content.
|
||||
- **Three equal card columns as feature row.** This is the most generic AI layout. Replace with a 2-column zig-zag, asymmetric grid, horizontal scroll, or masonry layout.
|
||||
- **Using `height: 100vh` for full-screen sections.** Replace with `min-height: 100dvh` to prevent layout jumping on mobile browsers (iOS Safari viewport bug).
|
||||
- **Complex flexbox percentage math.** Replace with CSS Grid for reliable multi-column structures.
|
||||
- **No max-width container.** Add a container constraint (around 1200-1440px) with auto margins so content doesn't stretch edge-to-edge on wide screens.
|
||||
- **Cards of equal height forced by flexbox.** Allow variable heights or use masonry when content varies in length.
|
||||
- **Uniform border-radius on everything.** Vary the radius: tighter on inner elements, softer on containers.
|
||||
- **No overlap or depth.** Elements sit flat next to each other. Use negative margins to create layering and visual depth.
|
||||
- **Symmetrical vertical padding.** Top and bottom padding are always identical. Adjust optically — bottom padding often needs to be slightly larger.
|
||||
- **Dashboard always has a left sidebar.** Try top navigation, a floating command menu, or a collapsible panel instead.
|
||||
- **Missing whitespace.** Double the spacing. Let the design breathe. Dense layouts work for data dashboards, not for marketing pages.
|
||||
- **Buttons not bottom-aligned in card groups.** When cards have different content lengths, CTAs end up at random heights. Pin buttons to the bottom of each card so they form a clean horizontal line regardless of content above.
|
||||
- **Feature lists starting at different vertical positions.** In pricing tables or comparison cards, the list of features should start at the same Y position across all columns. Use consistent spacing above the list or fixed-height title/price blocks.
|
||||
- **Inconsistent vertical rhythm in side-by-side elements.** When placing cards, columns, or panels next to each other, align shared elements (titles, descriptions, prices, buttons) across all items. Misaligned baselines make the layout look broken.
|
||||
- **Mathematical alignment that looks optically wrong.** Centering by the math doesn't always look centered to the eye. Icons next to text, play buttons in circles, or text in buttons often need 1-2px optical adjustments to feel right.
|
||||
|
||||
### Interactivity and States
|
||||
|
||||
- **No hover states on buttons.** Add background shift, slight scale, or translate on hover.
|
||||
- **No active/pressed feedback.** Add a subtle `scale(0.98)` or `translateY(1px)` on press to simulate a physical click.
|
||||
- **Instant transitions with zero duration.** Add smooth transitions (200-300ms) to all interactive elements.
|
||||
- **Missing focus ring.** Ensure visible focus indicators for keyboard navigation. This is an accessibility requirement, not optional.
|
||||
- **No loading states.** Replace generic circular spinners with skeleton loaders that match the layout shape.
|
||||
- **No empty states.** An empty dashboard showing nothing is a missed opportunity. Design a composed "getting started" view.
|
||||
- **No error states.** Add clear, inline error messages for forms. Do not use `window.alert()`.
|
||||
- **Dead links.** Buttons that link to `#`. Either link to real destinations or visually disable them.
|
||||
- **No indication of current page in navigation.** Style the active nav link differently so users know where they are.
|
||||
- **Scroll jumping.** Anchor clicks jump instantly. Add `scroll-behavior: smooth`.
|
||||
- **Animations using `top`, `left`, `width`, `height`.** Switch to `transform` and `opacity` for GPU-accelerated, smooth animation.
|
||||
|
||||
### Content
|
||||
|
||||
- **Generic names like "John Doe" or "Jane Smith".** Use diverse, realistic-sounding names.
|
||||
- **Fake round numbers like `99.99%`, `50%`, `$100.00`.** Use organic, messy data: `47.2%`, `$99.00`, `+1 (312) 847-1928`.
|
||||
- **Placeholder company names like "Acme Corp", "Nexus", "SmartFlow".** Invent contextual, believable brand names.
|
||||
- **AI copywriting cliches.** Never use "Elevate", "Seamless", "Unleash", "Next-Gen", "Game-changer", "Delve", "Tapestry", or "In the world of...". Write plain, specific language.
|
||||
- **Exclamation marks in success messages.** Remove them. Be confident, not loud.
|
||||
- **"Oops!" error messages.** Be direct: "Connection failed. Please try again."
|
||||
- **Passive voice.** Use active voice: "We couldn't save your changes" instead of "Mistakes were made."
|
||||
- **All blog post dates identical.** Randomize dates to appear real.
|
||||
- **Same avatar image for multiple users.** Use unique assets for every distinct person.
|
||||
- **Lorem Ipsum.** Never use placeholder latin text. Write real draft copy.
|
||||
- **Title Case On Every Header.** Use sentence case instead.
|
||||
|
||||
### Component Patterns
|
||||
|
||||
- **Generic card look (border + shadow + white background).** Remove the border, or use only background color, or use only spacing. Cards should exist only when elevation communicates hierarchy.
|
||||
- **Always one filled button + one ghost button.** Add text links or tertiary styles to reduce visual noise.
|
||||
- **Pill-shaped "New" and "Beta" badges.** Try square badges, flags, or plain text labels.
|
||||
- **Accordion FAQ sections.** Use a side-by-side list, searchable help, or inline progressive disclosure.
|
||||
- **3-card carousel testimonials with dots.** Replace with a masonry wall, embedded social posts, or a single rotating quote.
|
||||
- **Pricing table with 3 towers.** Highlight the recommended tier with color and emphasis, not just extra height.
|
||||
- **Modals for everything.** Use inline editing, slide-over panels, or expandable sections instead of popups for simple actions.
|
||||
- **Avatar circles exclusively.** Try squircles or rounded squares for a less generic look.
|
||||
- **Light/dark toggle always a sun/moon switch.** Use a dropdown, system preference detection, or integrate it into settings.
|
||||
- **Footer link farm with 4 columns.** Simplify. Focus on main navigational paths and legally required links.
|
||||
|
||||
### Iconography
|
||||
|
||||
- **Lucide or Feather icons exclusively.** These are the "default" AI icon choice. Use Phosphor, Heroicons, or a custom set for differentiation.
|
||||
- **Rocketship for "Launch", shield for "Security".** Replace cliche metaphors with less obvious icons (bolt, fingerprint, spark, vault).
|
||||
- **Inconsistent stroke widths across icons.** Audit all icons and standardize to one stroke weight.
|
||||
- **Missing favicon.** Always include a branded favicon.
|
||||
- **Stock "diverse team" photos.** Use real team photos, candid shots, or a consistent illustration style instead of uncanny stock imagery.
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Div soup.** Use semantic HTML: `<nav>`, `<main>`, `<article>`, `<aside>`, `<section>`.
|
||||
- **Inline styles mixed with CSS classes.** Move all styling to the project's styling system.
|
||||
- **Hardcoded pixel widths.** Use relative units (`%`, `rem`, `em`, `max-width`) for flexible layouts.
|
||||
- **Missing alt text on images.** Describe image content for screen readers. Never leave `alt=""` or `alt="image"` on meaningful images.
|
||||
- **Arbitrary z-index values like `9999`.** Establish a clean z-index scale in the theme/variables.
|
||||
- **Commented-out dead code.** Remove all debug artifacts before shipping.
|
||||
- **Import hallucinations.** Check that every import actually exists in `package.json` or the project dependencies.
|
||||
- **Missing meta tags.** Add proper `<title>`, `description`, `og:image`, and social sharing meta tags.
|
||||
|
||||
### Strategic Omissions (What AI Typically Forgets)
|
||||
|
||||
- **No legal links.** Add privacy policy and terms of service links in the footer.
|
||||
- **No "back" navigation.** Dead ends in user flows. Every page needs a way back.
|
||||
- **No custom 404 page.** Design a helpful, branded "page not found" experience.
|
||||
- **No form validation.** Add client-side validation for emails, required fields, and format checks.
|
||||
- **No "skip to content" link.** Essential for keyboard users. Add a hidden skip-link.
|
||||
- **No cookie consent.** If required by jurisdiction, add a compliant consent banner.
|
||||
|
||||
## Upgrade Techniques
|
||||
|
||||
When upgrading a project, pull from these high-impact techniques to replace generic patterns:
|
||||
|
||||
### Typography Upgrades
|
||||
- **Variable font animation.** Interpolate weight or width on scroll or hover for text that feels alive.
|
||||
- **Outlined-to-fill transitions.** Text starts as a stroke outline and fills with color on scroll entry or interaction.
|
||||
- **Text mask reveals.** Large typography acting as a window to video or animated imagery behind it.
|
||||
|
||||
### Layout Upgrades
|
||||
- **Broken grid / asymmetry.** Elements that deliberately ignore column structure — overlapping, bleeding off-screen, or offset with calculated randomness.
|
||||
- **Whitespace maximization.** Aggressive use of negative space to force focus on a single element.
|
||||
- **Parallax card stacks.** Sections that stick and physically stack over each other during scroll.
|
||||
- **Split-screen scroll.** Two halves of the screen sliding in opposite directions.
|
||||
|
||||
### Motion Upgrades
|
||||
- **Smooth scroll with inertia.** Decouple scrolling from browser defaults for a heavier, cinematic feel.
|
||||
- **Staggered entry.** Elements cascade in with slight delays, combining Y-axis translation with opacity fade. Never mount everything at once.
|
||||
- **Spring physics.** Replace linear easing with spring-based motion for a natural, weighty feel on all interactive elements.
|
||||
- **Scroll-driven reveals.** Content entering through expanding masks, wipes, or draw-on SVG paths tied to scroll progress.
|
||||
|
||||
### Surface Upgrades
|
||||
- **True glassmorphism.** Go beyond `backdrop-filter: blur`. Add a 1px inner border and a subtle inner shadow to simulate edge refraction.
|
||||
- **Spotlight borders.** Card borders that illuminate dynamically under the cursor.
|
||||
- **Grain and noise overlays.** A fixed, pointer-events-none overlay with subtle noise to break digital flatness.
|
||||
- **Colored, tinted shadows.** Shadows that carry the hue of the background rather than using generic black.
|
||||
|
||||
## Fix Priority
|
||||
|
||||
Apply changes in this order for maximum visual impact with minimum risk:
|
||||
|
||||
1. **Font swap** — biggest instant improvement, lowest risk
|
||||
2. **Color palette cleanup** — remove clashing or oversaturated colors
|
||||
3. **Hover and active states** — makes the interface feel alive
|
||||
4. **Layout and spacing** — proper grid, max-width, consistent padding
|
||||
5. **Replace generic components** — swap cliche patterns for modern alternatives
|
||||
6. **Add loading, empty, and error states** — makes it feel finished
|
||||
7. **Polish typography scale and spacing** — the premium final touch
|
||||
|
||||
## Rules
|
||||
|
||||
- Work with the existing tech stack. Do not migrate frameworks or styling libraries.
|
||||
- Do not break existing functionality. Test after every change.
|
||||
- Before importing any new library, check the project's dependency file first.
|
||||
- If the project uses Tailwind, check the version (v3 vs v4) before modifying config.
|
||||
- If the project has no framework, use vanilla CSS.
|
||||
- Keep changes reviewable and focused. Small, targeted improvements over big rewrites.
|
||||
1
.claude/skills/agent-browser
Symbolic link
1
.claude/skills/agent-browser
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.agents/skills/agent-browser
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,6 +8,8 @@ pom.xml.asc
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
nrepl-port
|
||||
.http-port
|
||||
resources/public/js/compiled
|
||||
*.log
|
||||
examples/
|
||||
|
||||
8
.opencode/opencode.json
Normal file
8
.opencode/opencode.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"edit": {
|
||||
"src/*": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
.opencode/package-lock.json
generated
22
.opencode/package-lock.json
generated
@@ -5,7 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.15.10"
|
||||
"@opencode-ai/plugin": "1.15.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
@@ -87,19 +87,19 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.10.tgz",
|
||||
"integrity": "sha512-V2p7CvpBtKWB+FID7Dl1y0Ci02zUT40A9b2RD9R9BOiuD8ZcKhHWov+irN0xVJA0Eg6OhEBfA0lPKRn1FNKPlw==",
|
||||
"version": "1.15.12",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.12.tgz",
|
||||
"integrity": "sha512-BBteGXEwJt+1ehHqQ+yKXmoWltrW+2xO++B1Fm/dnMGYWT9luEKA5RlUuVYA2qDF6uwlE7kmHZvQZAM79zWHEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.15.10",
|
||||
"@opencode-ai/sdk": "1.15.12",
|
||||
"effect": "4.0.0-beta.66",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.15",
|
||||
"@opentui/keymap": ">=0.2.15",
|
||||
"@opentui/solid": ">=0.2.15"
|
||||
"@opentui/core": ">=0.2.16",
|
||||
"@opentui/keymap": ">=0.2.16",
|
||||
"@opentui/solid": ">=0.2.16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
@@ -114,9 +114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.10.tgz",
|
||||
"integrity": "sha512-CUhpmMGGOqzvPnNNjjWmEIodAfP6Qnuki2ChIUKWYF7UImZ4zUcMZnzO5BtUxu/Ni1P8qzWxDioXs+7aIZQEhA==",
|
||||
"version": "1.15.12",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.12.tgz",
|
||||
"integrity": "sha512-lOaBNX93dkakZe6C42ttX1bkSx3K2c6+Yv+w8Qv02v5rPlu1vCXbmdfYDh9/bw+oq+NKPSaBm9d6kPA19hA5Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
|
||||
@@ -34,6 +34,14 @@ INTEGREAT_JOB="" lein run # Default: port 3000
|
||||
PORT=3449 lein run
|
||||
```
|
||||
|
||||
If you want to start the server, you should run `lein mcp-repl` which will output a nrepl-server port file and http-server port file.
|
||||
|
||||
## Browser Automation
|
||||
|
||||
When using the **agent-browser** skill for testing or automation:
|
||||
- Navigate to `/dev-login` to simulate an admin user and fake a session
|
||||
- Do not open directly to a specific page unless explicitly instructed to; instead, start on the dashboard and navigate from there
|
||||
|
||||
## Test Execution
|
||||
prefer using clojure-eval skill
|
||||
|
||||
|
||||
125
CLAUDE.md
125
CLAUDE.md
@@ -1,125 +1,2 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Integreat is a full-stack web application for accounts payable (AP) and accounting automation. It integrates with multiple financial data sources (Plaid, Yodlee, Square, Intuit QBO) to manage invoices, bank accounts, transactions, and vendor information.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Clojure 1.10.1 with Ring (Jetty), Datomic database, GraphQL (Lacinia, in process of depracation)
|
||||
- Frontend, two versions:
|
||||
* ClojureScript with Reagent, Re-frame, (almost depracated)
|
||||
* Server side: HTMX, TailwindCSS, alpinejs (current)
|
||||
- Java: Amazon Corretto 11 (required for Clojure 1.10.1)
|
||||
|
||||
## Development Commands
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
lein build # Create uberjar
|
||||
lein uberjar # Standalone JAR
|
||||
npm install # Install Node.js dependencies (for frontend build)
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
docker-compose up -d # Start Datomic, Solr services
|
||||
lein repl # Start Clojure REPL (nREPL on port 9000), typically one will be running for you already
|
||||
lein cljfmt check # Check code formatting
|
||||
lein cljfmt fix # Auto-format code
|
||||
clj-paren-repair [FILE ...] # fix parentheses in files
|
||||
clj-nrepl-eval -p PORT "(+ 1 2 3)" # evaluate clojure code
|
||||
|
||||
```
|
||||
|
||||
Often times, if a file won't compile, first clj-paren-repair on the file, then try again. If it doesn't wor still, try cljfmt check.
|
||||
|
||||
|
||||
**Running the Application:**
|
||||
|
||||
**As Web Server:**
|
||||
```bash
|
||||
INTEGREAT_JOB="" lein run # Default: port 3000
|
||||
# Or with custom port:
|
||||
PORT=3449 lein run
|
||||
```
|
||||
|
||||
**As Background Job:**
|
||||
Set `INTEGREAT_JOB` environment variable to one of:
|
||||
- `square-import-job` - Square POS transaction sync
|
||||
- `yodlee2` - Yodlee bank account sync
|
||||
- `plaid` - Plaid bank linking
|
||||
- `intuit` - Intuit QBO sync
|
||||
- `import-uploaded-invoices` - Process uploaded invoice PDFs
|
||||
- `ezcater-upsert` - EZcater PO sync
|
||||
- `ledger_reconcile` - Ledger reconciliation
|
||||
- `bulk_journal_import` - Journal entry import
|
||||
- (no job) - Run web server + nREPL
|
||||
|
||||
## Architecture
|
||||
|
||||
**Request Flow:**
|
||||
1. Ring middleware pipeline processes requests
|
||||
2. Authentication/authorization middleware (Buddy) wraps handlers
|
||||
3. Bidi routes dispatch to handlers
|
||||
4. SSR (server-side rendering) generates HTML with Hiccup for main views
|
||||
5. For interactive pages, HTMX handles partial updates
|
||||
6. Client-side uses alpinejs as a bonus
|
||||
|
||||
**Multi-tenancy:**
|
||||
- Client-based filtering via `:client/code` and `:client/groups`
|
||||
- Client selection via `X-Clients` header or session
|
||||
- Role-based permissions: admin, standard user, vendor
|
||||
|
||||
**Key Directories:**
|
||||
- `src/clj/auto_ap/` - Backend Clojure code
|
||||
- `src/clj/auto_ap/server.clj` - Main entry point, job dispatcher, Mount lifecycle
|
||||
- `src/clj/auto_ap/handler.clj` - Ring app, middleware stack
|
||||
- `src/clj/auto_ap/datomic/` - Datomic schema and queries
|
||||
- `src/clj/auto_ap/ssr/` - Server-side rendered page handlers (Hiccup templates)
|
||||
- `src/clj/auto_ap/routes/` - HTTP route definitions
|
||||
- `src/clj/auto_ap/jobs/` - Background batch jobs
|
||||
- `src/clj/auto_ap/graphql/` - GraphQL type definitions and resolvers
|
||||
- `src/cljs/auto_ap/` - Frontend ClojureScript for old, depracated version
|
||||
- `test/clj/auto_ap/` - Unit/integration tests
|
||||
|
||||
## Database
|
||||
|
||||
- Datomic schema defined in `resources/schema.edn`
|
||||
- Key entity patterns:
|
||||
- `:client/code`, `:client/groups` for multi-tenancy
|
||||
- `:vendor/*`, `:invoice/*`, `:transaction/*`, `:account/*` for standard entities
|
||||
- `:db/type/ref` for relationships, many with `:db/cardinality :db.cardinality/many`
|
||||
|
||||
## Configuration
|
||||
|
||||
- Dev config: `config/dev.edn` (set via `-Dconfig=config/dev.edn`)
|
||||
- Env vars: `INTEGREAT_JOB`, `PORT`
|
||||
- Docker: Uses Alpine-based Amazon Corretto 11 image
|
||||
|
||||
## Important Patterns
|
||||
|
||||
- **Middleware stack** in `handler.clj`: route matching → logging → client hydration → session/auth → idle timeout → error handling → gzip
|
||||
- **Client context** added by middleware: `:identity`, `:clients`, `:client`, `:matched-route`
|
||||
- **Job dispatching** in `server.clj`: checks `INTEGREAT_JOB` env var to run specific background jobs or start web server
|
||||
- **Test selectors**: namespaces ending in `integration` or `functional` are selected by `lein test :integration` / `lein test :functional`
|
||||
|
||||
## Clojure REPL Evaluation
|
||||
|
||||
The command `clj-nrepl-eval` is installed on your path for evaluating Clojure code via nREPL.
|
||||
|
||||
**Discover nREPL servers:**
|
||||
|
||||
`clj-nrepl-eval --discover-ports`
|
||||
|
||||
**Evaluate code:**
|
||||
|
||||
`clj-nrepl-eval -p <port> "<clojure-code>"`
|
||||
|
||||
With timeout (milliseconds)
|
||||
|
||||
`clj-nrepl-eval -p <port> --timeout 5000 "<clojure-code>"`
|
||||
|
||||
The REPL session persists between evaluations - namespaces and state are maintained.
|
||||
Always use `:reload` when requiring namespaces to pick up changes.
|
||||
@AGENTS.md
|
||||
|
||||
106
config/core.clj
Normal file
106
config/core.clj
Normal file
@@ -0,0 +1,106 @@
|
||||
(ns config.core
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.string :as s]
|
||||
[clojure.tools.logging :as log])
|
||||
(:import java.io.PushbackReader))
|
||||
|
||||
(defn parse-number [^String v]
|
||||
(try
|
||||
(Long/parseLong v)
|
||||
(catch NumberFormatException _
|
||||
(BigInteger. v))))
|
||||
|
||||
;originally found in cprop https://github.com/tolitius/cprop/blob/6963f8e04fd093744555f990c93747e0e5889395/src/cprop/source.cljc#L26
|
||||
(defn str->value
|
||||
"ENV vars and system properties are strings. str->value will convert:
|
||||
the numbers to longs, the alphanumeric values to strings, and will use Clojure reader for the rest
|
||||
in case reader can't read OR it reads a symbol, the value will be returned as is (a string)"
|
||||
[v]
|
||||
(cond
|
||||
(re-matches #"[0-9]+" v) (parse-number v)
|
||||
(re-matches #"^(true|false)$" v) (Boolean/parseBoolean v)
|
||||
(re-matches #"\w+" v) v
|
||||
:else
|
||||
(try
|
||||
(let [parsed (edn/read-string v)]
|
||||
(if (symbol? parsed) v parsed))
|
||||
(catch Throwable _ v))))
|
||||
|
||||
(defn keywordize [s]
|
||||
(-> (s/lower-case s)
|
||||
(s/replace "_" "-")
|
||||
(s/replace "." "-")
|
||||
(keyword)))
|
||||
|
||||
(defn read-system-env []
|
||||
(->> (System/getenv)
|
||||
(map (fn [[k v]] [(keywordize k) (str->value v)]))
|
||||
(into {})))
|
||||
|
||||
(defn read-system-props []
|
||||
(->> (System/getProperties)
|
||||
(map (fn [[k v]] [(keywordize k) (str->value v)]))
|
||||
(into {})))
|
||||
|
||||
(defn read-env-file [f]
|
||||
(try
|
||||
(when-let [env-file (io/file f)]
|
||||
(when (.exists env-file)
|
||||
(edn/read-string (slurp env-file))))
|
||||
(catch Exception e
|
||||
(log/warn (str "WARNING: failed to parse " f " " (.getLocalizedMessage e))))))
|
||||
|
||||
(defn read-config-file [f]
|
||||
(try
|
||||
(when-let [url (or (io/resource f) (io/file f))]
|
||||
(with-open [r (-> url io/reader PushbackReader.)]
|
||||
(edn/read r)))
|
||||
(catch java.io.FileNotFoundException _)
|
||||
(catch Exception e
|
||||
(log/warn (str "failed to parse " f " " (.getLocalizedMessage e))))))
|
||||
|
||||
(defn contains-in?
|
||||
"checks whether the nested key exists in a map"
|
||||
[m k-path]
|
||||
(let [one-before (get-in m (drop-last k-path))]
|
||||
(when (map? one-before) ;; in case k-path is "longer" than a map: {:a {:b {:c 42}}} => [:a :b :c :d]
|
||||
(contains? one-before (last k-path)))))
|
||||
|
||||
;; author of "deep-merge-with" is Chris Houser: https://github.com/clojure/clojure-contrib/commit/19613025d233b5f445b1dd3460c4128f39218741
|
||||
(defn deep-merge-with
|
||||
"Like merge-with, but merges maps recursively, appling the given fn
|
||||
only when there's a non-map at a particular level.
|
||||
(deepmerge + {:a {:b {:c 1 :d {:x 1 :y 2}} :e 3} :f 4}
|
||||
{:a {:b {:c 2 :d {:z 9} :z 3} :e 100}})
|
||||
-> {:a {:b {:z 3, :c 3, :d {:z 9, :x 1, :y 2}}, :e 103}, :f 4}"
|
||||
[f & maps]
|
||||
(apply
|
||||
(fn m [& maps]
|
||||
(if (every? map? maps)
|
||||
(apply merge-with m maps)
|
||||
(apply f maps)))
|
||||
(remove nil? maps)))
|
||||
|
||||
(defn merge-maps [& m]
|
||||
(reduce #(deep-merge-with (fn [_ v] v) %1 %2) m))
|
||||
|
||||
(defn load-env
|
||||
"Generate a map of environment variables."
|
||||
[& configs]
|
||||
(let [env-props (merge-maps (read-system-env) (read-system-props))]
|
||||
(apply
|
||||
merge-maps
|
||||
(read-config-file "config.edn")
|
||||
(read-env-file ".lein-env")
|
||||
(read-env-file (io/resource ".boot-env"))
|
||||
(read-env-file (:config env-props))
|
||||
env-props
|
||||
configs)))
|
||||
|
||||
(defonce
|
||||
^{:doc "A map of environment variables."}
|
||||
env (load-env))
|
||||
|
||||
(defn reload-env []
|
||||
(alter-var-root #'env (fn [_] (load-env))))
|
||||
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Phase I Trial: Django-Formtools Style Wizard (Server-Side Storage)
|
||||
|
||||
## Goal
|
||||
Create a **copy** of one existing wizard using Approach B (server-side session storage) without modifying any existing multi_modal.clj code. This is an isolated trial to validate the pattern.
|
||||
|
||||
## Trial Subject: Transaction Bulk Code Wizard
|
||||
**Why this one:**
|
||||
- Single-step form (simplest case)
|
||||
- Currently uses wizard protocols unnecessarily
|
||||
- ~420 lines in `transaction/bulk_code.clj`
|
||||
- Well-contained with clear inputs/outputs
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser Server
|
||||
| |
|
||||
|-- GET /bulk-code-trial -->|
|
||||
| |-- Create session entry
|
||||
| |-- Return form with wizard-id
|
||||
|<-- HTML + wizard-id -----|
|
||||
| |
|
||||
|-- POST (wizard-id + -->|
|
||||
| current-step data) |-- Validate step
|
||||
| |-- Store in session
|
||||
| |-- Return next step or done
|
||||
|<-- HTML -----------------|
|
||||
```
|
||||
|
||||
**Key difference from current approach:**
|
||||
- Current: EDN snapshot serialized in hidden form fields
|
||||
- Trial: Only `wizard-id` and current step fields in form. State lives server-side in atom.
|
||||
|
||||
---
|
||||
|
||||
## Files (All New - No Existing Files Modified)
|
||||
|
||||
```
|
||||
src/clj/auto_ap/ssr/components/wizard_trial/
|
||||
state.clj - Session storage backend
|
||||
core.clj - Trial wizard engine (minimal)
|
||||
|
||||
src/clj/auto_ap/ssr/transaction/
|
||||
bulk_code_trial.clj - Trial implementation of bulk code
|
||||
|
||||
test/clj/auto_ap/ssr/transaction/
|
||||
bulk_code_trial_test.clj - Tests for trial
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase I: Trial Implementation (This Week)
|
||||
|
||||
### Step 1: Create Session Storage
|
||||
|
||||
**File:** `src/clj/auto_ap/ssr/components/wizard_trial/state.clj`
|
||||
|
||||
```clojure
|
||||
(ns auto-ap.ssr.components.wizard-trial.state)
|
||||
|
||||
(defonce ^:private store (atom {}))
|
||||
|
||||
(defn create!
|
||||
"Creates new wizard session. Returns wizard-id."
|
||||
[initial-data]
|
||||
(let [id (str (java.util.UUID/randomUUID))]
|
||||
(swap! store assoc id {:data initial-data
|
||||
:created-at (java.util.Date.)})
|
||||
id))
|
||||
|
||||
(defn get-wizard
|
||||
"Retrieves wizard data by id."
|
||||
[id]
|
||||
(get @store id))
|
||||
|
||||
(defn update-step!
|
||||
"Merges step data into wizard session."
|
||||
[id step-key step-data]
|
||||
(swap! store assoc-in [id :data step-key] step-data))
|
||||
|
||||
(defn get-all-data
|
||||
"Returns merged data from all steps."
|
||||
[id]
|
||||
(-> (get-wizard id)
|
||||
:data
|
||||
vals
|
||||
(apply merge)))
|
||||
|
||||
(defn destroy!
|
||||
"Removes wizard session."
|
||||
[id]
|
||||
(swap! store dissoc id))
|
||||
```
|
||||
|
||||
### Step 2: Create Minimal Wizard Engine
|
||||
|
||||
**File:** `src/clj/auto_ap/ssr/components/wizard_trial/core.clj`
|
||||
|
||||
```clojure
|
||||
(ns auto-ap.ssr.components.wizard-trial.core
|
||||
(:require [auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[malli.core :as mc]))
|
||||
|
||||
(defn render-step
|
||||
"Renders a single step form."
|
||||
[{:keys [wizard-id step-config request]}]
|
||||
(let [step-data (get-in (ws/get-wizard wizard-id) [:data (:key step-config)])]
|
||||
[:form {:hx-post (:submit-route step-config)
|
||||
:hx-target "this"
|
||||
:hx-swap "outerHTML"}
|
||||
[:input {:type "hidden" :name "wizard-id" :value wizard-id}]
|
||||
[:input {:type "hidden" :name "step-key" :value (name (:key step-config))}]
|
||||
((:render step-config) (assoc request :step-data step-data))]))
|
||||
|
||||
(defn handle-submit
|
||||
"Handles step submission."
|
||||
[step-config request]
|
||||
(let [{:keys [wizard-id step-key]} (:form-params request)
|
||||
wizard-id (str wizard-id)
|
||||
step-key (keyword step-key)
|
||||
step-data (select-keys (:form-params request) (:fields step-config))]
|
||||
|
||||
(if-let [errors (mc/explain (:schema step-config) step-data)]
|
||||
;; Validation failed - re-render with errors
|
||||
(render-step {:wizard-id wizard-id
|
||||
:step-config step-config
|
||||
:request (assoc request :errors errors)})
|
||||
|
||||
;; Success - save and done (single step for trial)
|
||||
(let [all-data (ws/get-all-data wizard-id)]
|
||||
(ws/destroy! wizard-id)
|
||||
((:done-fn step-config) all-data request)))))
|
||||
```
|
||||
|
||||
### Step 3: Create Trial Bulk Code Form
|
||||
|
||||
**File:** `src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj`
|
||||
|
||||
```clojure
|
||||
(ns auto-ap.ssr.transaction.bulk-code-trial
|
||||
(:require [auto-ap.ssr.components.wizard-trial.core :as wt]
|
||||
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.utils :refer [html-response]]
|
||||
[auto-ap.datomic :refer [conn pull-attr]]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
(def bulk-code-schema
|
||||
[:map
|
||||
[:vendor {:optional true} [:maybe int?]]
|
||||
[:approval-status {:optional true} [:maybe keyword?]]
|
||||
[:accounts {:optional true}
|
||||
[:vector {:coerce? true}
|
||||
[:map
|
||||
[:account int?]
|
||||
[:location :string]
|
||||
[:percentage :double]]]]]])
|
||||
|
||||
(defn render-bulk-code-form
|
||||
"Pure function - renders the form given data."
|
||||
[{:keys [step-data errors]}]
|
||||
[:div.bulk-code-trial
|
||||
[:h2 "Bulk Code (Trial - Phase I)"]
|
||||
|
||||
[:div.space-y-4
|
||||
;; Vendor field
|
||||
[:div
|
||||
[:label "Vendor"]
|
||||
(com/typeahead {:name "vendor"
|
||||
:value (:vendor step-data)
|
||||
:url "/api/vendors/search"})]
|
||||
|
||||
;; Accounts table
|
||||
[:div
|
||||
[:h3 "Accounts"]
|
||||
[:table
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Account"]
|
||||
[:th "Location"]
|
||||
[:th "%"]]]
|
||||
[:tbody
|
||||
(for [[idx account] (map-indexed vector (:accounts step-data []))]
|
||||
[:tr {:key idx}
|
||||
[:td (com/typeahead {:name (str "accounts[" idx "][account]")
|
||||
:value (:account account)})]
|
||||
[:td (com/text-input {:name (str "accounts[" idx "][location]")
|
||||
:value (:location account)})]
|
||||
[:td (com/money-input {:name (str "accounts[" idx "][percentage]")
|
||||
:value (:percentage account)})]])]]]
|
||||
|
||||
;; Submit
|
||||
[:button {:type "submit"} "Apply Bulk Code"]]])
|
||||
|
||||
(def trial-step-config
|
||||
{:key :bulk-code
|
||||
:schema bulk-code-schema
|
||||
:fields [:vendor :approval-status :accounts]
|
||||
:render render-bulk-code-form
|
||||
:submit-route "/transaction/bulk-code-trial"
|
||||
:done-fn (fn [data request]
|
||||
;; Apply bulk coding logic here
|
||||
(html-response [:div.success "Bulk code applied! (Trial)"]))})
|
||||
|
||||
;; Route handlers
|
||||
(defn open-trial [request]
|
||||
(let [wizard-id (ws/create! {:accounts []})]
|
||||
(wt/render-step {:wizard-id wizard-id
|
||||
:step-config trial-step-config
|
||||
:request request})))
|
||||
|
||||
(defn submit-trial [request]
|
||||
(wt/handle-submit trial-step-config request))
|
||||
```
|
||||
|
||||
### Step 4: Add Routes
|
||||
|
||||
In your routes file (new entries, don't modify existing):
|
||||
|
||||
```clojure
|
||||
{::route/bulk-code-trial open-trial
|
||||
::route/bulk-code-trial-submit submit-trial}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase II: Expand Trial (If Phase I Works)
|
||||
|
||||
**Goal:** Test with a multi-step wizard
|
||||
|
||||
**Subject:** New Invoice Wizard (2-3 steps)
|
||||
- Step 1: Basic details
|
||||
- Step 2: Accounts (conditional)
|
||||
- Step 3: Submit
|
||||
|
||||
**New additions to trial engine:**
|
||||
- Step navigation (next/prev)
|
||||
- Conditional steps (skip accounts if not customizing)
|
||||
- Step validation per-step
|
||||
- Progress indicator
|
||||
|
||||
**Files to create:**
|
||||
```
|
||||
src/clj/auto_ap/ssr/invoice/
|
||||
new_invoice_trial.clj
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase III: Full Migration Decision (If Phase II Works)
|
||||
|
||||
**Goal:** Decide whether to migrate all wizards or keep both systems
|
||||
|
||||
**Evaluation criteria:**
|
||||
1. ✅ Line count reduction (target: 50%+)
|
||||
2. ✅ Testability (pure functions easier to test)
|
||||
3. ✅ Performance (server-side storage vs EDN serialization)
|
||||
4. ✅ Complexity (fewer protocols/middleware)
|
||||
5. ⚠️ Session handling (what happens on server restart?)
|
||||
6. ⚠️ Multiple tabs (can user have two wizards open?)
|
||||
|
||||
**Decision matrix:**
|
||||
|
||||
| Criteria | Current | Trial | Winner |
|
||||
|----------|---------|-------|--------|
|
||||
| Lines of code | ~8,200 | ~3,000 (est.) | Trial |
|
||||
| Server restarts | Survives (state in form) | Loses state | Current |
|
||||
| Multiple tabs | Works (independent forms) | Needs separate IDs | Tie |
|
||||
| Testability | Hard (cursor context) | Easy (pure functions) | Trial |
|
||||
| Complex merges | Painful | Simple (keyed steps) | Trial |
|
||||
|
||||
**If trial wins:** Migrate all wizards using Phase II pattern
|
||||
**If mixed:** Use trial for simple forms, keep current for complex multi-step
|
||||
|
||||
---
|
||||
|
||||
## How to Run the Trial
|
||||
|
||||
1. **Start server:** `lein run`
|
||||
2. **Navigate to:** `/transaction/bulk-code-trial`
|
||||
3. **Test:** Fill form, submit, verify state handling
|
||||
4. **Compare:** Open existing `/transaction/bulk-code` in another tab
|
||||
5. **Evaluate:** Which feels simpler? Which is easier to debug?
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria for Phase I
|
||||
|
||||
- [ ] Trial renders without errors
|
||||
- [ ] Form submission validates correctly
|
||||
- [ ] Server-side state persists across requests
|
||||
- [ ] No modifications to existing multi_modal.clj
|
||||
- [ ] Code is < 200 lines (vs 420 original)
|
||||
- [ ] Developer can understand flow in 5 minutes
|
||||
|
||||
**Ready to implement Phase I?**
|
||||
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,601 @@
|
||||
# Transaction Edit Modal: Simple / Advanced Mode Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the always-visible account split table in the transaction edit modal with a simple mode (single account + location fields) that is the default for uncoded or single-account transactions, with a toggle to the full advanced split table.
|
||||
|
||||
**Architecture:** HTMX-driven server-side swap. A new `edit-wizard-toggle-mode` GET endpoint re-renders the manual coding section in the requested mode. Mode is carried via a hidden `<input name="mode">` field included in all HTMX requests. `edit-vendor-changed` is updated to branch on mode. The `LinksStep` render function selects initial mode based on account row count.
|
||||
|
||||
**Tech Stack:** Clojure/Hiccup server-side rendering, HTMX, Alpine.js, Bidi routing, Datomic
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-27-transaction-edit-simple-advanced-mode-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | What changes |
|
||||
|------|-------------|
|
||||
| `src/cljc/auto_ap/routes/transactions.cljc` | Add `::edit-wizard-toggle-mode` route |
|
||||
| `src/clj/auto_ap/ssr/transaction/edit.clj` | Add `simple-mode-fields*`, `manual-coding-section*`, `edit-wizard-toggle-mode-handler`; update `LinksStep` render; update `edit-vendor-changed-handler`; register new route handler |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add the toggle-mode route
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/cljc/auto_ap/routes/transactions.cljc`
|
||||
|
||||
- [ ] **Step 1: Add the route entry**
|
||||
|
||||
Open `src/cljc/auto_ap/routes/transactions.cljc`. After the `"/edit-wizard-new-account"` line, add:
|
||||
|
||||
```clojure
|
||||
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
|
||||
```
|
||||
|
||||
The file should look like:
|
||||
|
||||
```clojure
|
||||
"/edit-wizard-new-account" ::edit-wizard-new-account
|
||||
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
|
||||
"/match-payment" ::link-payment
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.routes.transactions] :reload)"
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/cljc/auto_ap/routes/transactions.cljc
|
||||
git commit -m "feat: add edit-wizard-toggle-mode route"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add `simple-mode-fields*` — the simple-mode account/location UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
|
||||
|
||||
This function renders the account typeahead + location select + toggle link for simple mode. It goes near the existing `account-typeahead*` and `location-select*` helpers (around line 180).
|
||||
|
||||
- [ ] **Step 1: Add `simple-mode-fields*` after `account-typeahead*` (around line 180)**
|
||||
|
||||
```clojure
|
||||
(defn simple-mode-fields*
|
||||
"Renders the simple-mode account + location row and the toggle link.
|
||||
request must have :multi-form-state and :entity bound."
|
||||
[request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
client-id (or (-> request :entity :transaction/client :db/id)
|
||||
(:transaction/client snapshot))
|
||||
existing-row (first (:transaction/accounts snapshot))
|
||||
account-val (:transaction-account/account existing-row)
|
||||
location-val (or (:transaction-account/location existing-row) "Shared")
|
||||
account-id (when (nat-int? account-val)
|
||||
(dc/pull (dc/db conn) '[:account/location] account-val))
|
||||
row-id (or (:db/id existing-row) (str (java.util.UUID/randomUUID)))]
|
||||
[:div
|
||||
;; hidden inputs to encode the single row as transaction/accounts[0]
|
||||
(fc/with-field :transaction/accounts
|
||||
(fc/with-cursor-index 0
|
||||
[:span
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name) :value row-id}))
|
||||
[:div.flex.gap-2.mt-2
|
||||
(fc/with-field :transaction-account/account
|
||||
(com/validated-field
|
||||
{:label "Account" :errors (fc/field-errors)}
|
||||
[:div.w-72
|
||||
(account-typeahead* {:value account-val
|
||||
:client-id client-id
|
||||
:name (fc/field-name)
|
||||
:x-model "simpleAccountId"})]))
|
||||
(fc/with-field :transaction-account/location
|
||||
(com/validated-field
|
||||
{:label "Location"
|
||||
:errors (fc/field-errors)
|
||||
:x-hx-val:account-id "simpleAccountId"
|
||||
:hx-vals (hx/json (cond-> {:name (fc/field-name)}
|
||||
client-id (assoc :client-id client-id)))
|
||||
:x-dispatch:changed "simpleAccountId"
|
||||
:hx-trigger "changed"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
|
||||
:hx-target "find *"
|
||||
:hx-swap "outerHTML"}
|
||||
(location-select*
|
||||
{:name (fc/field-name)
|
||||
:account-location (:account/location account-id)
|
||||
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||
:value location-val})))
|
||||
;; hidden amount — full transaction total
|
||||
(fc/with-field :transaction-account/amount
|
||||
(let [total (Math/abs (or (-> request :entity :transaction/amount)
|
||||
(:transaction/amount snapshot)
|
||||
0.0))]
|
||||
(com/hidden {:name (fc/field-name) :value total})))]))
|
||||
;; toggle link
|
||||
[:div.mt-1
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
:hx-include "closest form"
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"}
|
||||
"Switch to advanced mode"]]]))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file has no parse errors**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/clj/auto_ap/ssr/transaction/edit.clj
|
||||
git commit -m "feat: add simple-mode-fields* for transaction edit modal"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Extract `manual-coding-section*` and update `LinksStep`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
|
||||
|
||||
Currently the manual coding block (vendor field + account grid) is inlined inside `LinksStep/render-step`. Extract it into `manual-coding-section*` which selects mode and renders accordingly. This also adds the `mode` hidden input and wraps the section in `#manual-coding-section`.
|
||||
|
||||
- [ ] **Step 1: Add `manual-mode-initial` helper** (determines initial mode from snapshot)
|
||||
|
||||
Add this function after `simple-mode-fields*`:
|
||||
|
||||
```clojure
|
||||
(defn- manual-mode-initial
|
||||
"Returns :simple or :advanced based on existing account row count."
|
||||
[snapshot]
|
||||
(let [rows (seq (:transaction/accounts snapshot))]
|
||||
(if (and rows (> (count rows) 1))
|
||||
:advanced
|
||||
:simple)))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `manual-coding-section*`**
|
||||
|
||||
Add after `manual-mode-initial`:
|
||||
|
||||
```clojure
|
||||
(defn manual-coding-section*
|
||||
"Renders the vendor field + account/location section for the manual tab.
|
||||
mode is :simple or :advanced."
|
||||
[mode request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
row-count (count (:transaction/accounts snapshot))]
|
||||
[:div#manual-coding-section
|
||||
;; hidden mode input — carried by all hx-include=\"closest form\" calls
|
||||
(com/hidden {:name "mode" :value (name mode)})
|
||||
;; vendor field
|
||||
[:div {:hx-trigger "change"
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest form"}
|
||||
(fc/with-field :transaction/vendor
|
||||
(com/validated-field
|
||||
{:label "Vendor" :errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
|
||||
;; account/location section
|
||||
(if (= mode :simple)
|
||||
[:div {:x-data (hx/json {:simpleAccountId
|
||||
(-> snapshot :transaction/accounts first
|
||||
:transaction-account/account)})}
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(simple-mode-fields* request)))]
|
||||
;; advanced mode
|
||||
[:div
|
||||
(when (<= row-count 1)
|
||||
[:div.mb-2
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
:hx-include "closest form"
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"}
|
||||
"Switch to simple mode"]])
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(fc/with-field :transaction/accounts
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)])))])]))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `LinksStep/render-step` to use `manual-coding-section*`**
|
||||
|
||||
In `LinksStep/render-step` (around line 826), replace the entire `[:div {}` block inside `[:div {:x-show "activeForm === 'manual'" ...}]` (which currently contains the vendor typeahead + approval status + `account-grid-body*`) with:
|
||||
|
||||
```clojure
|
||||
[:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
[:div {}
|
||||
(manual-coding-section* (manual-mode-initial snapshot) request)
|
||||
;; Approval status field
|
||||
(fc/with-field :transaction/approval-status
|
||||
(com/validated-field
|
||||
{:label "Status"
|
||||
:errors (fc/field-errors)}
|
||||
(let [current-value (name (or (fc/field-value) :transaction-approval-status/unapproved))]
|
||||
[:div {:x-data (hx/json {:approvalStatus current-value})}
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value current-value
|
||||
":value" "approvalStatus"})
|
||||
[:div {:class "inline-flex rounded-md shadow-sm", :role "group"}
|
||||
(com/button-group-button {"@click" "approvalStatus = 'approved'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }"
|
||||
:class "rounded-l-lg"}
|
||||
"Approved")
|
||||
(com/button-group-button {"@click" "approvalStatus = 'unapproved'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }"
|
||||
:class "rounded-r-lg"}
|
||||
"Unapproved")
|
||||
(com/button-group-button {"@click" "approvalStatus = 'suppressed'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }"
|
||||
:class "rounded-r-lg"}
|
||||
"Client Review")]]]))]]]
|
||||
```
|
||||
|
||||
Also remove the now-redundant `(fc/with-field :transaction/accounts ...)` wrapper that previously wrapped `account-grid-body*` (it is now handled inside `manual-coding-section*`).
|
||||
|
||||
- [ ] **Step 4: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/clj/auto_ap/ssr/transaction/edit.clj
|
||||
git commit -m "feat: extract manual-coding-section* with simple/advanced mode selection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add `edit-wizard-toggle-mode-handler`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
|
||||
|
||||
This handler re-renders `#manual-coding-section` in the opposite mode. It reads `mode` from the form params (via `step-params` in the decoded multi-form-state) and flips it.
|
||||
|
||||
- [ ] **Step 1: Add the handler function** (add after `edit-vendor-changed-handler`):
|
||||
|
||||
```clojure
|
||||
(defn edit-wizard-toggle-mode-handler [request]
|
||||
(let [step-params (-> request :multi-form-state :step-params)
|
||||
current-mode (keyword (or (:mode step-params) "simple"))
|
||||
target-mode (if (= current-mode :simple) :advanced :simple)
|
||||
snapshot (-> request :multi-form-state :snapshot)
|
||||
;; When switching simple→advanced, promote simple-mode values into accounts
|
||||
render-request
|
||||
(if (and (= target-mode :advanced)
|
||||
(= current-mode :simple))
|
||||
;; carry the simple-mode single row into snapshot so the table shows it
|
||||
(let [accounts (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot)))]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
|
||||
(vec accounts))
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts]
|
||||
(vec accounts))))
|
||||
;; advanced→simple: take first row only
|
||||
(let [first-row (first (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot))))]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
|
||||
(if first-row [first-row] []))
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts]
|
||||
(if first-row [first-row] [])))))]
|
||||
(html-response
|
||||
(fc/start-form (:multi-form-state render-request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* target-mode render-request))))))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register the handler in `key->handler`**
|
||||
|
||||
In the `key->handler` map (around line 1357), add after the `::route/edit-wizard-new-account` entry:
|
||||
|
||||
```clojure
|
||||
::route/edit-wizard-toggle-mode (-> edit-wizard-toggle-mode-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/clj/auto_ap/ssr/transaction/edit.clj
|
||||
git commit -m "feat: add edit-wizard-toggle-mode-handler"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update `edit-vendor-changed-handler` to support both modes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
|
||||
|
||||
Currently this handler always returns `[:div#account-grid-body ...]`. It must now return `#manual-coding-section` in the correct mode.
|
||||
|
||||
- [ ] **Step 1: Replace `edit-vendor-changed-handler`**
|
||||
|
||||
Replace the entire `edit-vendor-changed-handler` function body with:
|
||||
|
||||
```clojure
|
||||
(defn edit-vendor-changed-handler [request]
|
||||
(let [multi-form-state (:multi-form-state request)
|
||||
snapshot (:snapshot multi-form-state)
|
||||
step-params (:step-params multi-form-state)
|
||||
mode (keyword (or (:mode step-params) "simple"))
|
||||
client-id (or (:transaction/client snapshot)
|
||||
(-> request :entity :transaction/client :db/id))
|
||||
vendor-id (or (:transaction/vendor step-params)
|
||||
(:transaction/vendor snapshot))
|
||||
total (Math/abs (or (-> request :entity :transaction/amount)
|
||||
(:transaction/amount snapshot)
|
||||
0.0))
|
||||
amount-mode (or (:amount-mode snapshot) "$")
|
||||
existing-accounts (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot)))
|
||||
default-account (when (and (empty? existing-accounts) vendor-id client-id)
|
||||
(vendor-default-account vendor-id client-id))
|
||||
render-request
|
||||
(if (and (empty? existing-accounts) vendor-id client-id)
|
||||
(let [new-account (cond-> {:db/id (str (java.util.UUID/randomUUID))
|
||||
:transaction-account/location (or (:account/location default-account) "Shared")
|
||||
:transaction-account/amount (if (= amount-mode "%") 100.0 total)}
|
||||
default-account (assoc :transaction-account/account (:db/id default-account)))]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts] [new-account])
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
|
||||
request)]
|
||||
(html-response
|
||||
(fc/start-form (:multi-form-state render-request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* mode render-request))))))
|
||||
```
|
||||
|
||||
Note: the `hx-target` on the vendor field in `manual-coding-section*` must point to `#manual-coding-section` (not `#account-grid-body`) — this was set correctly in Task 3.
|
||||
|
||||
- [ ] **Step 2: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/clj/auto_ap/ssr/transaction/edit.clj
|
||||
git commit -m "feat: update edit-vendor-changed-handler to support simple/advanced mode"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Fix `fc/with-cursor-index` usage in `simple-mode-fields*`
|
||||
|
||||
**Context:** The simple-mode fields need to emit form names like `step-params[transaction/accounts][0][transaction-account/account]`. The existing `fc/cursor-map` used in `account-grid-body*` handles this automatically. For the single-row simple mode we need to manually set index 0.
|
||||
|
||||
Look up how `fc/with-cursor-index` (or equivalent) works in `src/clj/auto_ap/ssr/form_cursor.clj` before writing the code in Task 2. If no such helper exists, use `fc/cursor-nth` or replicate the index manually via the form cursor API.
|
||||
|
||||
- [ ] **Step 1: Inspect the form cursor API**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-vars 'auto-ap.ssr.form-cursor)"
|
||||
```
|
||||
|
||||
Note the available functions.
|
||||
|
||||
- [ ] **Step 2: Update `simple-mode-fields*` if needed**
|
||||
|
||||
If `fc/with-cursor-index` does not exist, replace the `fc/with-cursor-index 0` call in Task 2 with the correct form-cursor idiom. The key requirement is that the hidden `db/id`, `transaction-account/account`, `transaction-account/location`, and `transaction-account/amount` fields emit names matching index 0 of `transaction/accounts`.
|
||||
|
||||
A known working pattern from `account-grid-body*`:
|
||||
|
||||
```clojure
|
||||
(fc/cursor-map #(transaction-account-row* {:value % ...}))
|
||||
```
|
||||
|
||||
For simple mode with a single synthetic row, build a one-element vector in the snapshot and let `fc/cursor-map` iterate it — but render a flat div instead of a table. Or pass the cursor manually:
|
||||
|
||||
```clojure
|
||||
(fc/with-field :transaction/accounts
|
||||
(let [row-cursor (fc/cursor-nth 0)] ; adjust to actual API
|
||||
(fc/with-cursor row-cursor
|
||||
...field rendering...)))
|
||||
```
|
||||
|
||||
Verify field names are correct in a browser after implementation.
|
||||
|
||||
- [ ] **Step 3: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit if any changes were made**
|
||||
|
||||
```bash
|
||||
git add src/clj/auto_ap/ssr/transaction/edit.clj
|
||||
git commit -m "fix: correct form-cursor indexing for simple-mode account field"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Manual smoke test
|
||||
|
||||
Before writing automated tests, verify the UI works end-to-end in a browser.
|
||||
|
||||
- [ ] **Step 1: Start the application**
|
||||
|
||||
```bash
|
||||
INTEGREAT_JOB="" lein run
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Open a transaction with no accounts**
|
||||
|
||||
Navigate to a transaction with no coded accounts. Open the edit modal. Verify it opens in simple mode with blank account and location fields.
|
||||
|
||||
- [ ] **Step 3: Test vendor selection in simple mode**
|
||||
|
||||
Select a vendor. Verify the account field is populated with the vendor's default account and the location is set appropriately.
|
||||
|
||||
- [ ] **Step 4: Test toggle to advanced**
|
||||
|
||||
Click "Switch to advanced mode". Verify the full split table appears with one pre-populated row.
|
||||
|
||||
- [ ] **Step 5: Test toggle back to simple**
|
||||
|
||||
With 1 row, click "Switch to simple mode". Verify the single account/location fields appear with that row's values.
|
||||
|
||||
- [ ] **Step 6: Test with a split transaction**
|
||||
|
||||
Open a transaction that already has 2+ accounts. Verify it opens in advanced mode. Verify the "Switch to simple mode" link is absent.
|
||||
|
||||
- [ ] **Step 7: Test save round-trip**
|
||||
|
||||
In simple mode, set a vendor, account, and location. Save. Re-open. Verify the same values are pre-populated in simple mode.
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Write e2e tests
|
||||
|
||||
**Files:**
|
||||
- Create: `test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj`
|
||||
|
||||
Check how existing e2e tests are structured first:
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-ns)"
|
||||
```
|
||||
|
||||
Look for test namespaces matching `auto-ap.ssr.transaction.*` or `auto-ap.e2e.*`. Follow the same fixture/helper patterns.
|
||||
|
||||
The test file should cover all 20 acceptance criteria from the spec. Group them with `testing` blocks:
|
||||
|
||||
```clojure
|
||||
(ns auto-ap.ssr.transaction.edit-simple-advanced-mode-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
;; ... project-specific test helpers ...
|
||||
))
|
||||
|
||||
(deftest simple-advanced-mode-initial-state
|
||||
(testing "AC1: uncoded transaction opens in simple mode"
|
||||
;; create a transaction with no accounts
|
||||
;; open edit modal
|
||||
;; verify #manual-coding-section has mode=simple hidden input
|
||||
;; verify no #account-grid-body present
|
||||
(is ...))
|
||||
|
||||
(testing "AC2: single-account transaction opens in simple mode with values pre-populated"
|
||||
...)
|
||||
|
||||
(testing "AC3: multi-account transaction opens in advanced mode"
|
||||
...))
|
||||
|
||||
(deftest simple-mode-vendor-selection
|
||||
(testing "AC4: selecting vendor populates account and location"
|
||||
...)
|
||||
(testing "AC5: selecting vendor does not overwrite manually chosen account"
|
||||
...))
|
||||
|
||||
(deftest mode-toggle
|
||||
(testing "AC9: switching to advanced carries account/location into first row"
|
||||
...)
|
||||
(testing "AC10: switching to advanced from blank simple gives empty table"
|
||||
...)
|
||||
(testing "AC11: switch-to-simple link visible with 0 or 1 rows"
|
||||
...)
|
||||
(testing "AC12: switch-to-simple link absent with 2+ rows"
|
||||
...)
|
||||
(testing "AC13: switching to simple pre-populates from first row"
|
||||
...))
|
||||
|
||||
(deftest save-round-trip
|
||||
(testing "AC6: save in simple mode persists vendor/account/location"
|
||||
...)
|
||||
(testing "AC18: switching modes mid-edit then saving produces valid transaction"
|
||||
...)
|
||||
(testing "AC19: split transaction re-opens in advanced mode with splits intact"
|
||||
...)
|
||||
(testing "AC20: single-account transaction re-opens in simple mode"
|
||||
...))
|
||||
```
|
||||
|
||||
Fill in actual test bodies using the project's test infrastructure (browser automation or ring mock depending on what exists).
|
||||
|
||||
- [ ] **Step 1: Check existing test conventions**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.testing-conventions] :reload)"
|
||||
```
|
||||
|
||||
Also load the testing-conventions skill for guidance:
|
||||
|
||||
```
|
||||
Load skill: testing-conventions
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the test file** following project conventions
|
||||
|
||||
- [ ] **Step 3: Run the tests**
|
||||
|
||||
```bash
|
||||
clj-nrepl-eval -p 9000 "(clojure.test/run-tests 'auto-ap.ssr.transaction.edit-simple-advanced-mode-test)"
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj
|
||||
git commit -m "test: add e2e acceptance tests for simple/advanced mode"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist (completed inline)
|
||||
|
||||
- **Spec coverage:** All 20 ACs addressed — Tasks 2–5 implement the behaviour; Task 8 tests all 20.
|
||||
- **Placeholder scan:** Task 6 and Task 8 contain some "fill in" guidance — this is intentional because they depend on runtime API discovery. The instructions tell the engineer exactly where to look and what to verify.
|
||||
- **Type consistency:** `manual-coding-section*` is used consistently by `LinksStep/render-step`, `edit-vendor-changed-handler`, and `edit-wizard-toggle-mode-handler`. `#manual-coding-section` is the swap target throughout. `mode` hidden input uses `(name mode)` for string serialization and `(keyword ...)` for deserialization.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Transaction Edit Modal: Simple / Advanced Mode
|
||||
|
||||
**Date:** 2026-05-27
|
||||
**Status:** Approved
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The transaction editing modal gains a two-mode interface. **Simple mode** replaces the account/location split table with two single fields (account typeahead + location dropdown), suitable for the common case of a single-account transaction. **Advanced mode** exposes the existing split table for multi-account allocations. The mode is selected automatically on open based on the transaction's current state, and the user can toggle between modes via a server-rendered swap.
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
### Simple mode
|
||||
|
||||
```
|
||||
[ Vendor typeahead ]
|
||||
[ Account typeahead ] [ Location ▼ ]
|
||||
Switch to advanced mode →
|
||||
[ Memo ]
|
||||
[ Approval status buttons ]
|
||||
```
|
||||
|
||||
### Advanced mode
|
||||
|
||||
```
|
||||
[ Vendor typeahead ]
|
||||
Switch to simple mode →
|
||||
[ Account | Location | $ / % | ✕ ]
|
||||
[ Account | Location | $ / % | ✕ ]
|
||||
[ + Add row ]
|
||||
[ Memo ]
|
||||
[ Approval status buttons ]
|
||||
```
|
||||
|
||||
The toggle link sits directly below the vendor field. It reads "Switch to advanced mode" in simple mode and "Switch to simple mode" in advanced mode. In advanced mode with 2+ rows, the "Switch to simple mode" link is hidden (the user must remove rows manually before they can return to simple mode). The toggle link fires `hx-get` on the `::route/edit-wizard-toggle-mode` endpoint with `hx-include="closest form"` so the current form state (including `mode`) is carried in the request.
|
||||
|
||||
---
|
||||
|
||||
## Mode Selection on Open
|
||||
|
||||
The server determines initial mode when rendering the `LinksStep` body:
|
||||
|
||||
- **0 or 1 existing account rows** → render simple mode, pre-populate the account/location fields from the existing row (blank if none).
|
||||
- **2+ existing account rows** → render advanced mode with all rows populated.
|
||||
|
||||
---
|
||||
|
||||
## Toggle Mechanism (Option B — HTMX swap)
|
||||
|
||||
Clicking the toggle link fires an `hx-get` request to a new endpoint that re-renders the editable body of the modal in the target mode. Mode is passed as a query param (e.g., `?mode=advanced` or `?mode=simple`).
|
||||
|
||||
**Simple → Advanced:** The current account and location values from the simple fields are carried into the first row of the advanced table (100% of transaction total, or full dollar amount). Any additional rows previously added to the table are preserved via the multi-form-state snapshot.
|
||||
|
||||
**Advanced → Simple:** Only available when there is exactly 0 or 1 row in the table. The toggle link is absent when 2+ rows exist.
|
||||
|
||||
The swapped fragment replaces the entire editable body div (`#links-step-body` or equivalent target), keeping the side panel and modal chrome intact.
|
||||
|
||||
The current mode is tracked as a hidden `<input name="mode" value="simple|advanced">` inside the form. This ensures all HTMX calls that `hx-include` the form (vendor change, toggle, submit) carry the mode value without requiring it to be a separate query param.
|
||||
|
||||
---
|
||||
|
||||
## Vendor Selection Behaviour
|
||||
|
||||
### In simple mode
|
||||
|
||||
When the vendor typeahead fires its `change` event, the existing `edit-vendor-changed` HTMX endpoint is called. The response re-renders the simple-mode body with:
|
||||
|
||||
- The account field populated with the vendor's default account (clientized for the transaction's client).
|
||||
- The location field set to the account's fixed location, or "Shared" if the account has no fixed location.
|
||||
- Fields are editable; the user may override both.
|
||||
|
||||
The vendor default only applies when there are no existing accounts (matching existing server-side logic in `edit-vendor-changed-handler`). If the user has already manually chosen an account, changing vendor does not overwrite it.
|
||||
|
||||
### In advanced mode
|
||||
|
||||
Vendor change behaviour is unchanged from the current implementation: if no rows exist, a single row is created with the vendor's default account and location at 100% / full amount. If rows already exist, the vendor change has no effect on the table.
|
||||
|
||||
---
|
||||
|
||||
## Form Submission
|
||||
|
||||
Both modes submit to the same `edit-submit` endpoint. Simple mode submits the single account and location as a one-element `transaction/accounts` vector, identical in shape to what the advanced table produces today. No schema or handler changes are needed for submission.
|
||||
|
||||
---
|
||||
|
||||
## New Routes
|
||||
|
||||
| Method | Route key | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `GET` | `::route/edit-wizard-toggle-mode` | Re-renders the editable body in the requested mode. Reads current form state from `hx-include`'d form fields; the `mode` hidden input indicates the target mode (the endpoint flips it). |
|
||||
|
||||
The `edit-vendor-changed` endpoint reads the `mode` hidden input from the included form to determine whether to return simple-mode or advanced-mode HTML.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
These are the expected behaviours to be verified by e2e tests:
|
||||
|
||||
1. **Verify** that when a transaction has no coded accounts, it opens in simple mode with blank account and location fields.
|
||||
2. **Verify** that when a transaction has exactly one coded account, it opens in simple mode with that account and location pre-selected.
|
||||
3. **Verify** that when a transaction has two or more coded accounts, it opens in advanced mode showing the full split table.
|
||||
4. **Verify** that in simple mode, selecting a vendor replaces the account field with the vendor's default account (clientized for the transaction's client) and sets the location to the account's fixed location or "Shared".
|
||||
5. **Verify** that in simple mode, selecting a vendor does not overwrite an account the user has already manually chosen (i.e., the account typeahead already has a value when the vendor change fires).
|
||||
6. **Verify** that after saving in simple mode, re-opening the transaction shows the same vendor, account, and location that were saved.
|
||||
7. **Verify** that in simple mode, the account field is a typeahead that respects the same allowance rules as the advanced table (`:account/default-allowance`).
|
||||
8. **Verify** that in simple mode, the location dropdown shows the account's fixed location (sole option) if the account has one, or the full list of client locations plus "Shared" if it does not.
|
||||
9. **Verify** that clicking "Switch to advanced mode" from simple mode re-renders the form in advanced mode with one table row pre-populated from the simple-mode account and location fields.
|
||||
10. **Verify** that clicking "Switch to advanced mode" from a blank simple mode (no account selected) re-renders the form in advanced mode with an empty table (no rows, just the "Add row" button).
|
||||
11. **Verify** that the "Switch to simple mode" link is visible in advanced mode when there is exactly 0 or 1 row.
|
||||
12. **Verify** that the "Switch to simple mode" link is absent in advanced mode when there are 2 or more rows.
|
||||
13. **Verify** that clicking "Switch to simple mode" from advanced mode (1 row) re-renders the form in simple mode with that row's account and location pre-populated.
|
||||
14. **Verify** that in advanced mode, selecting a vendor when there are no rows creates a single row with the vendor's default account, correct location, and 100% (or full dollar amount) allocation.
|
||||
15. **Verify** that in advanced mode, selecting a vendor when rows already exist does not modify the existing rows.
|
||||
16. **Verify** that the vendor default account is determined by clientizing the vendor for the client the transaction belongs to (client-specific account override takes precedence over the global vendor default).
|
||||
17. **Verify** that the approval status, memo, and vendor fields are present and functional in both simple and advanced modes.
|
||||
18. **Verify** that switching modes mid-edit and then saving produces a valid transaction (no orphaned or duplicated account rows).
|
||||
19. **Verify** that a transaction saved in advanced mode with splits can be re-opened and remains in advanced mode with all splits intact.
|
||||
20. **Verify** that a transaction saved in simple mode (single account) can be re-opened in simple mode and the single account/location are correctly pre-populated.
|
||||
|
||||
---
|
||||
|
||||
## Files Affected
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/clj/auto_ap/ssr/transaction/edit.clj` | Add simple-mode rendering functions; add `edit-wizard-toggle-mode` handler; update `edit-vendor-changed-handler` to support both modes; update `LinksStep` body render to select initial mode |
|
||||
| `src/cljc/auto_ap/routes/transactions.cljc` | Add `::edit-wizard-toggle-mode` route |
|
||||
| E2E test file (to be created) | Acceptance criteria tests for all 20 items above |
|
||||
@@ -118,7 +118,12 @@
|
||||
"type": "local",
|
||||
"command": ["clojure", "-Tmcp", "start", ":config-profile", ":cli-assist"],
|
||||
"enabled": true
|
||||
}
|
||||
} ,
|
||||
"tavily": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.tavily.com/mcp/?tavilyApiKey=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",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"read": "allow",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
:aliases {"build" ["do" ["uberjar"]]
|
||||
#_#_"fig:dev" ["run" "-m" "figwheel.main" "-b" "dev" "-r"]
|
||||
"build-dev" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"]
|
||||
"mcp-repl" ["trampoline" "run" "-m" "dev-mcp"]
|
||||
#_#_"fig:min" ["run" "-m" "figwheel.main" "-O" "whitespace" "-bo" "min"]}
|
||||
|
||||
|
||||
|
||||
@@ -2,43 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-size: 15px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Tabular numbers for monetary values */
|
||||
.tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Focus ring for accessibility */
|
||||
*:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
button,
|
||||
a.button,
|
||||
.navbar-item,
|
||||
.pagination-link,
|
||||
.tag,
|
||||
select,
|
||||
input,
|
||||
textarea {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.htmx-added .fade-in {
|
||||
opacity: 0.0 !important;
|
||||
}
|
||||
@@ -188,7 +151,7 @@
|
||||
}
|
||||
|
||||
.min-h-content {
|
||||
min-height: calc(100dvh - 4em);
|
||||
min-height: calc(100vh - 4em);
|
||||
}
|
||||
|
||||
.arrow,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
form.dz .notification { border: 2px dashed lightgray;}
|
||||
|
||||
html,body {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
background-color: #f9fafb;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
@@ -468,125 +468,4 @@ table.balance-sheet th.total {
|
||||
background-color: whitesmoke !important;
|
||||
border-color: whitesmoke !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ===== Design Upgrades ===== */
|
||||
|
||||
/* Better table row hover */
|
||||
.table tbody tr {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
.table tbody tr:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Sidebar item hover */
|
||||
.aside .main .item:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
.aside .main .item.is-active {
|
||||
background-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
/* Navbar item hover */
|
||||
.navbar-item:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Button active/pressed state */
|
||||
button:active,
|
||||
a.button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Pagination link hover */
|
||||
.pagination-link:hover {
|
||||
background-color: #e5e7eb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
/* Tag hover */
|
||||
.tag.is-delete:hover {
|
||||
background-color: #ef4444 !important;
|
||||
}
|
||||
|
||||
/* Modal card shadow */
|
||||
.modal-card {
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15) !important;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Card elevation */
|
||||
.card {
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Sidebar viewport fix */
|
||||
.aside {
|
||||
min-height: calc(100dvh - 46px) !important;
|
||||
}
|
||||
|
||||
/* Better select styling */
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
|
||||
background-position: right 0.5rem center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: 1.5em 1.5em !important;
|
||||
padding-right: 2.5rem !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
/* Notification polish */
|
||||
.notification {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Better loader color */
|
||||
.loader.is-loading {
|
||||
border-color: #16a34a !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Input focus state */
|
||||
.input:focus, .textarea:focus, .select select:focus {
|
||||
border-color: #3b82f6 !important;
|
||||
box-shadow: 0 0 0 1px #3b82f6 !important;
|
||||
}
|
||||
|
||||
/* Better title styling */
|
||||
.title {
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.title.is-4 {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Better code/mono font for numbers */
|
||||
.table td {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Subtle scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
@@ -5,10 +5,8 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Integreat</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
|
||||
<link href="/css/font.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/bulma.min.css" />
|
||||
<link rel="stylesheet" href="/css/bulma-calendar.min.css" />
|
||||
<link rel="stylesheet" href="/css/bulma-badge.min.css" />
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,47 +7,11 @@
|
||||
"skillPath": "skills/agent-browser/SKILL.md",
|
||||
"computedHash": "228f87d57035100d9dc6efcfc05aafd4b6e3962adacaa04b8217ab2fadb15dc8"
|
||||
},
|
||||
"brandkit": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"frontend-design": {
|
||||
"source": "anthropics/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brandkit/SKILL.md",
|
||||
"computedHash": "b63012f3c3d21197e0185d3e9cc7ec40c589fb10e0b5a32a561739de31aa3f20"
|
||||
},
|
||||
"design-taste-frontend": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill/SKILL.md",
|
||||
"computedHash": "6d838b246d0e35d0b53f4f23f98ba7a1dd561937e64f7d0c7553b0928e376c3e"
|
||||
},
|
||||
"high-end-visual-design": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/soft-skill/SKILL.md",
|
||||
"computedHash": "7db385e4c5370e5a7fca9704a1361b056e4504ea6a03924bb86f33a4f00b5c73"
|
||||
},
|
||||
"industrial-brutalist-ui": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brutalist-skill/SKILL.md",
|
||||
"computedHash": "8fc355c4aadb7d29c53ca28bc41be3cd6eea765d121e3737c4dc2d0f90a8effa"
|
||||
},
|
||||
"minimalist-ui": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/minimalist-skill/SKILL.md",
|
||||
"computedHash": "08873a3131d3be27bef9bf3304b310b16b44ca6e3561aebe532797be3443f6bd"
|
||||
},
|
||||
"redesign-existing-projects": {
|
||||
"source": "leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/redesign-skill/SKILL.md",
|
||||
"computedHash": "b405eee0e0e80fc243f731d9aa368bca307e356db7e6157d27101d369dac6726"
|
||||
},
|
||||
"taste-skill": {
|
||||
"source": "nexu-io/open-design",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill/SKILL.md",
|
||||
"computedHash": "7d17b9f0f0a7d48d0f63354a717db0c3e7bc6470ff0d73b861f28c74de81d87b"
|
||||
"skillPath": "skills/frontend-design/SKILL.md",
|
||||
"computedHash": "063a0e6448123cd359ad0044cc46b0e490cc7964d45ef4bb9fd842bd2ffbca67"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,13 +263,6 @@
|
||||
(into new-session)
|
||||
(assoc :client-selection client-selection))))))))
|
||||
|
||||
(defn wrap-dev-bypass-auth
|
||||
[handler]
|
||||
(fn [request]
|
||||
(if (= "dev" (:dd-env env))
|
||||
(handler (assoc request :identity {:user/role "admin" :user/name "Dev User"}))
|
||||
(handler request))))
|
||||
|
||||
(defn wrap-gunzip-jwt
|
||||
[handler]
|
||||
(fn [{:keys [session] :as request}]
|
||||
@@ -322,6 +315,21 @@
|
||||
:valid-trimmed-client-ids trimmed-clients
|
||||
:first-client-id (first valid-clients)
|
||||
:clients-trimmed? (not= (count trimmed-clients) (count valid-clients)))))))
|
||||
|
||||
(defn wrap-dev-login [handler]
|
||||
(fn [request]
|
||||
(if (and (= "/dev-login" (:uri request))
|
||||
(some-> env :base-url (.contains "localhost")))
|
||||
(let [identity {:user "Dev User"
|
||||
:user/name "Dev User"
|
||||
:user/role "admin"
|
||||
:db/id 0}]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body "<p>Logged in as Dev User!</p><a href='/dashboard'>Continue to dashboard</a>"
|
||||
:session {:identity identity
|
||||
:version session-version/current-session-version}})
|
||||
(handler request))))
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defonce app
|
||||
(-> route-handler
|
||||
@@ -332,9 +340,11 @@
|
||||
(wrap-hydrate-clients)
|
||||
(wrap-store-client-in-session)
|
||||
(wrap-gunzip-jwt)
|
||||
(wrap-dev-login)
|
||||
(wrap-authorization auth-backend)
|
||||
(wrap-dev-bypass-auth)
|
||||
(wrap-authentication auth-backend (session-backend {:authfn (fn [auth] (dissoc auth :exp))}))
|
||||
(wrap-authentication auth-backend
|
||||
(session-backend {:authfn (fn [auth]
|
||||
(dissoc auth :exp))}))
|
||||
|
||||
#_(wrap-pprint-session)
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
(.setHandler server stats-handler))
|
||||
(.setStopAtShutdown server true))
|
||||
|
||||
(mount/defstate port :start (Integer/parseInt (str (or (env :port) "3000"))))
|
||||
(def ^:dynamic *http-port-override* nil)
|
||||
|
||||
(mount/defstate port :start (Integer/parseInt (str (or *http-port-override* (env :port) "3000"))))
|
||||
|
||||
(mount/defstate jetty
|
||||
:start (run-jetty app {:port port
|
||||
@@ -82,7 +84,7 @@
|
||||
(statsd/gauge "requests.5xx" (double (.getResponses5xx (.getHandler jetty))))
|
||||
(.statsReset (.getHandler jetty))
|
||||
(catch Exception e
|
||||
(alog/warn ::cant-collect-stats :error e))))
|
||||
(alog/warn ::cant-collect-stats :error e))))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(mount/defstate jetty-stats
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
(ns auto-ap.ssr.auth
|
||||
(:require
|
||||
[auto-ap.session-version :as session-version]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[config.core :refer [env]]
|
||||
[hiccup2.core :as hiccup]
|
||||
[hiccup.util :as hu]))
|
||||
|
||||
(defn logout [request]
|
||||
@@ -37,81 +36,73 @@
|
||||
"scope" "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"}
|
||||
next (assoc "state" (hu/url-encode next))))))))
|
||||
|
||||
(defn- login-page [contents]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (str "<!DOCTYPE html>"
|
||||
(hiccup/html
|
||||
[:html
|
||||
[:head
|
||||
[:meta {:charset "utf-8"}]
|
||||
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
|
||||
[:title "Integreat · Sign In"]
|
||||
[:link {:rel "icon" :type "image/png" :href "/favicon.png"}]
|
||||
[:link {:rel "stylesheet" :href "/output.css"}]
|
||||
[:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}]
|
||||
[:style
|
||||
"body{background:linear-gradient(160deg,#79b52e 0%,#009cea 100%);min-height:100vh}"]]
|
||||
[:body contents]]))})
|
||||
|
||||
(defn- page-contents [request]
|
||||
[:div#app {"@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
[:div
|
||||
{:x-data (hx/json {:showError false
|
||||
:errorDetails ""})
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"}
|
||||
|
||||
:x-data (hx/json {:showError false
|
||||
:errorDetails ""
|
||||
:showNotification false
|
||||
:notificationDetails ""})
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"}
|
||||
[:div#app-contents.flex.overflow-hidden
|
||||
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "}
|
||||
[:div#notification-holder
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification"}
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400
|
||||
{"@click" "showNotification=false"}
|
||||
svg/filled-x]]
|
||||
[:div.fixed.top-0.left-0.right-0.z-50.mx-auto.max-w-md.w-full.px-4.pt-6
|
||||
{:x-show "showError"
|
||||
"x-transition:enter" "transition duration-200 ease-out"
|
||||
"x-transition:enter-start" "opacity-0 -translate-y-3"
|
||||
"x-transition:enter-end" "opacity-100 translate-y-0"}
|
||||
[:div.relative.bg-white.rounded-xl.shadow-xl.border.border-red-200.p-4
|
||||
[:button.absolute.right-3.top-3.p-1.text-red-400.hover:text-red-600
|
||||
{"@click" "showError=false"}
|
||||
svg/filled-x]
|
||||
[:div.flex.items-start.gap-3
|
||||
[:div.flex-shrink-0.w-5.h-5.text-red-500 svg/alert]
|
||||
[:div.flex-1.min-w-0
|
||||
[:p.text-sm.font-medium.text-gray-900 "Something went wrong"]
|
||||
[:p.text-xs.text-gray-500.mt-0.5
|
||||
"Our team has been notified. Please try again."
|
||||
[:span {:x-data (hx/json {"e" false})}
|
||||
" "
|
||||
[:a.text-xs.underline.cursor-pointer.text-gray-500.hover:text-gray-700
|
||||
{"@click" "e=true"}
|
||||
"Details"]
|
||||
[:pre.text-xs.mt-1.font-mono.text-red-600.bg-red-50.p-2.rounded {:x-show "e" :x-text "errorDetails"}]]]]]]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showNotification"
|
||||
"x-transition:enter" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:enter-start" "opacity-0 translate-y-full"
|
||||
"x-transition:enter-end" "opacity-100 translate-y-0"
|
||||
"x-transition:leave" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:leave-start" "opacity-100 translate-y-0"
|
||||
"x-transition:leave-end" "opacity-0 translate-y-full"}
|
||||
[:div.flex.items-center.justify-center.min-h-screen.px-4
|
||||
[:div.w-full.max-w-lg
|
||||
[:div.flex.flex-col.items-center.mb-10
|
||||
[:img {:src "/img/logo-big.png" :alt "Integreat" :class "h-16 brightness-0 invert"}]]
|
||||
|
||||
[:div {:class "p-4 text-lg w-full" :role "alert"}
|
||||
[:div.text-sm
|
||||
[:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]]
|
||||
[:div {:x-show "showError"
|
||||
:x-init ""}
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600
|
||||
{"@click" "showError=false"}
|
||||
svg/filled-x]]
|
||||
[:div.bg-white.rounded-2xl.shadow-2xl.p-10
|
||||
{:style "animation: slideUp 0.4s ease-out forwards; opacity: 0;"}
|
||||
[:div.flex.flex-col.items-center.gap-8
|
||||
[:div.text-center
|
||||
[:h1.text-2xl.font-bold.text-gray-900 "Sign in to Integreat"]
|
||||
[:p.mt-2.text-base.text-gray-500 "Use your Google account to continue"]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showError"
|
||||
"x-transition:enter" "transition duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
[:a {:href (login-url (get (:query-params request) "redirect-to"))
|
||||
:class "w-full max-w-xs flex items-center justify-center gap-3 px-6 py-3.5 text-base font-semibold rounded-xl border-2 border-gray-200 text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400 transition-all duration-150"}
|
||||
svg/google
|
||||
"Sign in with Google"]]
|
||||
|
||||
[:div {:class "p-4 mb-4 text-lg w-full" :role "alert"}
|
||||
[:div.inline-block.w-8.h-8.mr-2 svg/alert]
|
||||
[:span.font-medium "Oh, drat! An unexpected error has occurred."]
|
||||
[:div.text-sm {:x-data (hx/json {"expandError" false})}
|
||||
[:p "Integreat staff have been notified and are looking into it. "]
|
||||
[:p "To see error details, " [:a.underline.cursor-pointer {"@click" "expandError=true"} "click here"] "."]
|
||||
[:pre#error-details.text-xs {:x-show "expandError" :x-text "errorDetails"}]]]]]]
|
||||
[:div {:class "relative w-full h-screen overflow-hidden"}
|
||||
[:div {:class "absolute inset-0"
|
||||
:style "background: linear-gradient(135deg, #7ECDC0 0%, #A8D85C 50%, #7ECDC0 100%);"}]
|
||||
[:div {:class "absolute inset-0"
|
||||
:style "background: radial-gradient(circle at 20% 80%, rgba(255,255,255,0.35) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,0.25) 0%, transparent 40%);"}]
|
||||
[:div {:class "absolute inset-0 flex items-center justify-center"}
|
||||
[:div {:class "animate-slideUp"
|
||||
:style "background: rgba(255,255,255,0.92); backdrop-filter: blur(20px); border-radius: 20px; padding: 40px 36px; width: 340px; box-shadow: 0 25px 80px rgba(0,0,0,0.15); text-align: center;"}
|
||||
[:img {:src "/img/logo-big.png"
|
||||
:style "display: block; margin: 0 auto 24px;"}]
|
||||
[:a {:href (login-url (get (:query-params request) "redirect-to"))
|
||||
:style "display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-width: 224px; height: 44px; padding: 0 24px; border: 1px solid #747775; border-radius: 22px; background: #FFFFFF; text-decoration: none; transition: background-color 200ms ease, box-shadow 200ms ease, transform 150ms ease; cursor: pointer;"
|
||||
:onmouseover "this.style.background='#F5F5F5'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'; this.style.transform='translateY(-1px)';"
|
||||
:onmouseout "this.style.background='#FFFFFF'; this.style.boxShadow='none'; this.style.transform='translateY(0)';"
|
||||
:onmousedown "this.style.transform='scale(0.98)';"
|
||||
:onmouseup "this.style.transform='translateY(-1px)';"}
|
||||
svg/google-g
|
||||
[:span {:style "font-family: 'Roboto', Arial, sans-serif; font-weight: 500; font-size: 14px; line-height: 20px; color: #1F1F1F; white-space: nowrap;"}
|
||||
"Sign in with Google"]]
|
||||
[:p {:style "margin-top: 16px; font-size: 11px; color: #aaa; margin-bottom: 0;"}
|
||||
"Secure & fast"]]]]]]])
|
||||
[:p.mt-2.text-center.text-xs.text-gray-400
|
||||
"By signing in, you agree to our "
|
||||
[:a.underline.hover:text-gray-600 {:href "/terms"} "Terms of Service"]
|
||||
" and "
|
||||
[:a.underline.hover:text-gray-600 {:href "/privacy"} "Privacy Policy"]]]]]])
|
||||
|
||||
(defn login [request]
|
||||
(base-page
|
||||
request
|
||||
(page-contents request)
|
||||
|
||||
"Dashboard"))
|
||||
(login-page (page-contents request)))
|
||||
54
src/clj/auto_ap/ssr/components/wizard_trial/core.clj
Normal file
54
src/clj/auto_ap/ssr/components/wizard_trial/core.clj
Normal file
@@ -0,0 +1,54 @@
|
||||
(ns auto-ap.ssr.components.wizard-trial.core
|
||||
(:require
|
||||
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[auto-ap.ssr.utils :refer [html-response main-transformer modal-response]]
|
||||
[malli.core :as mc]
|
||||
[malli.error :as me]))
|
||||
|
||||
(defn render-step
|
||||
"Renders a single step form.
|
||||
step-config is a map with:
|
||||
- :key - step keyword
|
||||
- :render - function taking {:keys [step-data errors request]} returning hiccup
|
||||
- :submit-route - string URL for form POST"
|
||||
[{:keys [wizard-id step-config request errors]}]
|
||||
(let [{:keys [key render submit-route]} step-config
|
||||
wizard (ws/get-wizard wizard-id)
|
||||
step-data (get-in wizard [:data key] {})]
|
||||
[:form {:hx-post submit-route
|
||||
:hx-target "this"}
|
||||
[:input {:type "hidden" :name "wizard-id" :value wizard-id}]
|
||||
[:input {:type "hidden" :name "step-key" :value (name key)}]
|
||||
(render {:step-data step-data :errors errors :request request})]))
|
||||
|
||||
(defn handle-submit
|
||||
"Handles step submission.
|
||||
Validates step data against schema.
|
||||
If valid: saves to session and calls done-fn.
|
||||
If invalid: re-renders step with errors."
|
||||
[step-config request]
|
||||
(let [{:keys [form-params]} request
|
||||
wizard-id (get form-params "wizard-id")
|
||||
step-key (keyword (get form-params "step-key"))
|
||||
fields (:fields step-config)
|
||||
step-data (reduce (fn [acc field]
|
||||
(if-let [v (get form-params (name field))]
|
||||
(assoc acc field v)
|
||||
acc))
|
||||
{}
|
||||
fields)
|
||||
schema (:schema step-config)
|
||||
decoded (mc/decode schema step-data main-transformer)
|
||||
valid? (mc/validate schema decoded)]
|
||||
(if valid?
|
||||
(do
|
||||
(ws/update-step! wizard-id step-key decoded)
|
||||
(let [all-data (ws/get-all-data wizard-id)]
|
||||
(ws/destroy! wizard-id)
|
||||
((:done-fn step-config) all-data request)))
|
||||
(let [errors (me/humanize (mc/explain schema decoded))]
|
||||
(modal-response
|
||||
(render-step {:wizard-id wizard-id
|
||||
:step-config step-config
|
||||
:request request
|
||||
:errors errors}))))))
|
||||
35
src/clj/auto_ap/ssr/components/wizard_trial/state.clj
Normal file
35
src/clj/auto_ap/ssr/components/wizard_trial/state.clj
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns auto-ap.ssr.components.wizard-trial.state)
|
||||
|
||||
(defonce ^:private store (atom {}))
|
||||
|
||||
(defn create!
|
||||
"Creates new wizard session with initial data. Returns wizard-id string."
|
||||
[initial-data]
|
||||
(let [id (str (java.util.UUID/randomUUID))]
|
||||
(swap! store assoc id {:data initial-data
|
||||
:created-at (java.util.Date.)})
|
||||
id))
|
||||
|
||||
(defn get-wizard
|
||||
"Retrieves wizard data by id. Returns nil if not found."
|
||||
[id]
|
||||
(get @store id))
|
||||
|
||||
(defn update-step!
|
||||
"Merges step data into wizard session under step-key."
|
||||
[id step-key step-data]
|
||||
(swap! store update-in [id :data step-key] merge step-data))
|
||||
|
||||
(defn get-all-data
|
||||
"Returns merged data from all steps for final submission."
|
||||
[id]
|
||||
(when-let [wizard (get-wizard id)]
|
||||
(let [data (:data wizard)]
|
||||
(apply merge
|
||||
(into {} (remove (comp map? val) data))
|
||||
(filter map? (vals data))))))
|
||||
|
||||
(defn destroy!
|
||||
"Removes wizard session."
|
||||
[id]
|
||||
(swap! store dissoc id))
|
||||
@@ -523,10 +523,9 @@
|
||||
[:path {:d "M4.5 9.5h15s1 0 1 1v12s0 1 -1 1h-15s-1 0 -1 -1v-12s0 -1 1 -1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
|
||||
[:path {:d "M6.5 6a5.5 5.5 0 0 1 11 0v3.5h-11Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
|
||||
|
||||
(def google-g
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24", :width "20", :height "20"}
|
||||
[:path {:d "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z", :fill "#4285F4"}]
|
||||
[:path {:d "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", :fill "#34A853"}]
|
||||
[:path {:d "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", :fill "#FBBC05"}]
|
||||
[:path {:d "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", :fill "#EA4335"}]
|
||||
[:path {:d "M1.5 12h10.5v3H1.5z", :fill "none"}]])
|
||||
(def google
|
||||
[:svg {:viewbox "0 0 24 24", :width "20", :height "20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:fill "#4285F4" :d "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}]
|
||||
[:path {:fill "#34A853" :d "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}]
|
||||
[:path {:fill "#FBBC05" :d "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}]
|
||||
[:path {:fill "#EA4335" :d "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"}]])
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
||||
[auto-ap.ssr.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
|
||||
[auto-ap.ssr.transaction.bulk-code-trial :as bulk-code-trial]
|
||||
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
|
||||
grid-page query-schema
|
||||
wrap-status-from-source]]
|
||||
@@ -53,7 +54,7 @@
|
||||
all-selected
|
||||
(:ids (fetch-ids (dc/db conn) (-> request
|
||||
(assoc-in [:form-params :start] 0)
|
||||
(assoc-in [:form-params :per-page] 250))))
|
||||
(assoc-in [:form-params :per-page] 250))))
|
||||
:else
|
||||
selected)
|
||||
all-ids (all-ids-not-locked ids)
|
||||
@@ -101,16 +102,18 @@
|
||||
(def key->handler
|
||||
(merge edit/key->handler
|
||||
bulk-code/key->handler
|
||||
{::route/bulk-code-trial bulk-code-trial/open-trial
|
||||
::route/bulk-code-trial-submit bulk-code-trial/submit-trial}
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/page page
|
||||
{::route/page page
|
||||
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
|
||||
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
|
||||
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
||||
::route/table table
|
||||
::route/csv csv
|
||||
::route/table table
|
||||
::route/csv csv
|
||||
::route/bank-account-filter bank-account-filter
|
||||
::route/bulk-delete (-> bulk-delete
|
||||
(wrap-schema-enforce :form-schema query-schema))}
|
||||
::route/bulk-delete (-> bulk-delete
|
||||
(wrap-schema-enforce :form-schema query-schema))}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
|
||||
172
src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj
Normal file
172
src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj
Normal file
@@ -0,0 +1,172 @@
|
||||
(ns auto-ap.ssr.transaction.bulk-code-trial
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn pull-attr pull-many]]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.wizard-trial.core :as wt]
|
||||
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead* location-select*]]
|
||||
[auto-ap.ssr.utils :refer [html-response modal-response percentage]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[malli.core :as mc]))
|
||||
|
||||
(def bulk-code-schema
|
||||
(mc/schema
|
||||
[:map
|
||||
[:vendor {:optional true} [:maybe int?]]
|
||||
[:approval-status {:optional true} [:maybe keyword?]]
|
||||
[:accounts {:optional true}
|
||||
[:vector {:coerce? true}
|
||||
[:map
|
||||
[:account int?]
|
||||
[:location :string]
|
||||
[:percentage percentage]]]]]))
|
||||
|
||||
(defn- account-row
|
||||
"Renders a single account row with typeahead, location select, percentage, and delete button."
|
||||
[index {:keys [account location percentage]} errors request]
|
||||
(let [account-name (str "accounts[" index "][account]")
|
||||
location-name (str "accounts[" index "][location]")
|
||||
percentage-name (str "accounts[" index "][percentage]")
|
||||
row-errors (get errors index)
|
||||
client-id (-> request :clients first :db/id)
|
||||
account-location (try
|
||||
(when (nat-int? account)
|
||||
(:account/location (dc/pull (dc/db conn) '[:account/location] account)))
|
||||
(catch Exception e nil))
|
||||
client-locations (try
|
||||
(pull-attr (dc/db conn) :client/locations client-id)
|
||||
(catch Exception e nil))]
|
||||
[:tr
|
||||
[:td (com/validated-field
|
||||
{:errors (get row-errors :account)}
|
||||
(account-typeahead* {:value account
|
||||
:client-id client-id
|
||||
:name account-name}))]
|
||||
[:td (com/validated-field
|
||||
{:errors (get row-errors :location)}
|
||||
(location-select* {:name location-name
|
||||
:account-location account-location
|
||||
:client-locations client-locations
|
||||
:value location}))]
|
||||
[:td (com/validated-field
|
||||
{:errors (get row-errors :percentage)}
|
||||
(com/money-input {:name percentage-name
|
||||
:value (some-> percentage (* 100) long)
|
||||
:class "w-16"}))]
|
||||
[:td (com/a-icon-button {"@click.prevent.stop" "this.closest('tr').remove()"}
|
||||
svg/x)]]))
|
||||
|
||||
(defn render-bulk-code-form
|
||||
"Renders the bulk code form inside a modal card structure.
|
||||
Takes {:keys [step-data errors request]}"
|
||||
[{:keys [step-data errors request]}]
|
||||
(let [vendor (get step-data :vendor)
|
||||
approval-status (get step-data :approval-status)
|
||||
accounts (get step-data :accounts
|
||||
[{:account nil :location "Shared" :percentage 0.5}
|
||||
{:account nil :location "Shared" :percentage 0.5}
|
||||
{:account nil :location "" :percentage nil}])
|
||||
selected-ids [] ; Would come from request in real implementation
|
||||
all-ids []]
|
||||
(com/modal-card-advanced
|
||||
{:class "md:w-[750px] md:h-[600px] w-full h-full"}
|
||||
(com/modal-header {}
|
||||
[:div.p-2 "Bulk editing " (count all-ids) " transactions"])
|
||||
(com/modal-body {}
|
||||
[:div.space-y-4.p-4
|
||||
[:div.grid.grid-cols-2.gap-4
|
||||
;; Vendor field
|
||||
[:div
|
||||
(com/validated-field
|
||||
{:label "Vendor"
|
||||
:errors (get errors :vendor)}
|
||||
(com/typeahead {:name "vendor"
|
||||
:placeholder "Search for vendor..."
|
||||
:url (bidi/path-for auto-ap.ssr-routes/only-routes :vendor-search)
|
||||
:value vendor
|
||||
:content-fn (fn [c]
|
||||
(try
|
||||
(pull-attr (dc/db conn) :vendor/name c)
|
||||
(catch Exception e
|
||||
"Vendor")))}))]
|
||||
|
||||
;; Approval status field
|
||||
[:div
|
||||
(com/validated-field
|
||||
{:label "Status"
|
||||
:errors (get errors :approval-status)}
|
||||
(com/select {:name "approval-status"
|
||||
:value (some-> approval-status name)
|
||||
:allow-blank? true
|
||||
:options [["" "No Change"]
|
||||
["approved" "Approved"]
|
||||
["unapproved" "Unapproved"]
|
||||
["suppressed" "Suppressed"]
|
||||
["requires_feedback" "Requires Feedback"]]}))]]
|
||||
|
||||
;; Accounts section
|
||||
[:div.col-span-2.pt-4
|
||||
[:h3.text-lg.font-medium.mb-3 "Expense Accounts"]
|
||||
(com/validated-field
|
||||
{:errors (get errors :accounts)}
|
||||
[:table.w-full.text-sm.text-left
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Account"]
|
||||
[:th {:class "w-32"} "Location"]
|
||||
[:th {:class "w-16"} "%"]
|
||||
[:th {:class "w-16"}]]]
|
||||
[:tbody
|
||||
(map-indexed
|
||||
(fn [idx account]
|
||||
(account-row idx account (get errors :accounts) request))
|
||||
accounts)]])]
|
||||
|
||||
;; Add new account button
|
||||
[:div
|
||||
(com/button {:color :secondary
|
||||
:type "button"
|
||||
:class "mt-2"
|
||||
"@click" (str "
|
||||
const tbody = this.closest('form').querySelector('tbody');
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.innerHTML = `
|
||||
<td><input type='text' name='accounts[" (count accounts) "][account]' placeholder='Account ID' class='w-full'></td>
|
||||
<td><input type='text' name='accounts[" (count accounts) "][location]' value='Shared' class='w-full'></td>
|
||||
<td><input type='number' name='accounts[" (count accounts) "][percentage]' class='w-16'></td>
|
||||
<td><button type='button' onclick='this.closest(\"tr\").remove()'>×</button></td>
|
||||
`;
|
||||
tbody.appendChild(newRow);
|
||||
")}
|
||||
"New account")]])
|
||||
|
||||
(com/modal-footer {}
|
||||
[:div.flex.justify-end
|
||||
[:div.flex.items-baseline.gap-x-4
|
||||
(com/form-errors {:errors (seq errors)})
|
||||
(com/button {:color :primary :type "submit" :class "w-32"} "Save")]]))))
|
||||
|
||||
(def trial-step-config
|
||||
{:key :bulk-code
|
||||
:schema bulk-code-schema
|
||||
:fields [:vendor :approval-status :accounts]
|
||||
:render render-bulk-code-form
|
||||
:submit-route "/transaction/bulk-code-trial"
|
||||
:done-fn (fn [data request]
|
||||
(modal-response
|
||||
(com/success-modal {:title "Transactions Coded (Trial)"}
|
||||
[:p "This was a trial run. No transactions were actually modified."])
|
||||
:headers {"hx-trigger" "refreshTable"}))})
|
||||
|
||||
(defn open-trial [request]
|
||||
(let [wizard-id (ws/create! {})]
|
||||
(modal-response
|
||||
(wt/render-step {:wizard-id wizard-id
|
||||
:step-config trial-step-config
|
||||
:request request}))))
|
||||
|
||||
(defn submit-trial [request]
|
||||
(wt/handle-submit trial-step-config request))
|
||||
@@ -440,6 +440,12 @@
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#transaction-filters"}
|
||||
"Code")
|
||||
(com/button {:color :secondary
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-code-trial)
|
||||
:hx-target "#modal-holder"
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#transaction-filters"}
|
||||
"Code (Trial)")
|
||||
(com/button {:color :primary
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
||||
:hx-target "#modal-holder"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns auto-ap.ssr.transaction.edit
|
||||
(:require
|
||||
[auto-ap.cursor :as cursor]
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact conn pull-attr pull-ref]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
@@ -179,6 +180,82 @@
|
||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||
client-id)))})])
|
||||
|
||||
(defn simple-mode-fields*
|
||||
"Renders the simple-mode account + location row and the toggle-to-advanced link.
|
||||
Must be called within a fc/start-form + fc/with-field :step-params context.
|
||||
Caller must establish Alpine x-data with simpleAccountId in scope."
|
||||
[request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
step-params (-> request :multi-form-state :step-params)
|
||||
client-id (or (-> request :entity :transaction/client :db/id)
|
||||
(:transaction/client snapshot))
|
||||
existing-row (first (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot))))
|
||||
account-val (let [av (:transaction-account/account existing-row)]
|
||||
(if (map? av) (:db/id av) av))
|
||||
location-val (or (:transaction-account/location existing-row) "Shared")
|
||||
account-id (when (nat-int? account-val)
|
||||
(dc/pull (dc/db conn) '[:account/location] account-val))
|
||||
row-id (or (:db/id existing-row) (str (java.util.UUID/randomUUID)))
|
||||
total (Math/abs (or (-> request :entity :transaction/amount)
|
||||
(:transaction/amount snapshot)
|
||||
0.0))]
|
||||
[:div
|
||||
(fc/with-field :transaction/accounts
|
||||
(fc/with-cursor (let [cur fc/*current*]
|
||||
(if (sequential? @cur)
|
||||
(nth cur 0 nil)
|
||||
(auto_ap.cursor.MapCursor. {} (cursor/state cur) (conj (cursor/path cur) 0))))
|
||||
[:span
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value row-id}))
|
||||
[:div.flex.gap-2.mt-2
|
||||
(fc/with-field :transaction-account/account
|
||||
(com/validated-field
|
||||
{:label "Account"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-72
|
||||
(account-typeahead* {:value account-val
|
||||
:client-id client-id
|
||||
:name (fc/field-name)
|
||||
:x-model "simpleAccountId"})]))
|
||||
(fc/with-field :transaction-account/location
|
||||
(com/validated-field
|
||||
{:label "Location"
|
||||
:errors (fc/field-errors)
|
||||
:x-hx-val:account-id "simpleAccountId"
|
||||
:hx-vals (hx/json (cond-> {:name (fc/field-name)}
|
||||
client-id (assoc :client-id client-id)))
|
||||
:x-dispatch:changed "simpleAccountId"
|
||||
:hx-trigger "changed"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
|
||||
:hx-target "find *"
|
||||
:hx-swap "outerHTML"}
|
||||
(location-select*
|
||||
{:name (fc/field-name)
|
||||
:account-location (:account/location account-id)
|
||||
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||
:value location-val})))
|
||||
(fc/with-field :transaction-account/amount
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value total}))]]))
|
||||
[:div.mt-1
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
:hx-include "closest form"
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"}
|
||||
"Switch to advanced mode"]]]))
|
||||
|
||||
(defn- manual-mode-initial
|
||||
"Returns :simple or :advanced based on existing account row count."
|
||||
[snapshot]
|
||||
(let [rows (seq (:transaction/accounts snapshot))]
|
||||
(if (and rows (> (count rows) 1))
|
||||
:advanced
|
||||
:simple)))
|
||||
|
||||
(defn transaction-account-row* [{:keys [value client-id amount-mode total]}]
|
||||
(com/data-grid-row
|
||||
(-> {:class "account-row"
|
||||
@@ -420,6 +497,55 @@
|
||||
(format "$%,.2f" total))
|
||||
(com/data-grid-cell {})))))
|
||||
|
||||
(defn manual-coding-section*
|
||||
"Renders the vendor field + account/location section for the manual tab.
|
||||
mode is :simple or :advanced.
|
||||
In simple mode, establishes Alpine x-data with simpleAccountId in scope.
|
||||
Must be called within a fc/start-form + fc/with-field :step-params context."
|
||||
[mode request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
step-params (-> request :multi-form-state :step-params)
|
||||
all-accounts (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot)))
|
||||
row-count (count all-accounts)]
|
||||
[:div#manual-coding-section
|
||||
(com/hidden {:name "mode" :value (name mode)})
|
||||
[:div {:hx-trigger "change"
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest form"}
|
||||
(fc/with-field :transaction/vendor
|
||||
(com/validated-field
|
||||
{:label "Vendor" :errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
|
||||
(if (= mode :simple)
|
||||
[:div {:x-data (hx/json {:simpleAccountId
|
||||
(let [av (-> (first all-accounts) :transaction-account/account)]
|
||||
(if (map? av) (:db/id av) av))})}
|
||||
(simple-mode-fields* request)]
|
||||
[:div
|
||||
(when (<= row-count 1)
|
||||
[:div.mb-2
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
:hx-include "closest form"
|
||||
:hx-target "#manual-coding-section"
|
||||
:hx-swap "outerHTML"}
|
||||
"Switch to simple mode"]])
|
||||
(fc/with-field :transaction/accounts
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)}
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)]))])]))
|
||||
|
||||
(defn toggle-amount-mode [request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
old-mode (or (:amount-mode snapshot) "$")
|
||||
@@ -815,7 +941,6 @@
|
||||
":disabled" "!canChange"}
|
||||
"Manual"))]
|
||||
[:div {:x-show "activeForm === 'link-payment'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
|
||||
(payment-matches-view request)]
|
||||
[:div {:x-show "activeForm === 'link-unpaid-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
(unpaid-invoices-view request)]
|
||||
@@ -825,27 +950,7 @@
|
||||
(transaction-rules-view request)]
|
||||
[:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
[:div {}
|
||||
[:div {:hx-trigger "change"
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
|
||||
:hx-target "#account-grid-body"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest form"}
|
||||
(fc/with-field :transaction/vendor
|
||||
(com/validated-field
|
||||
{:label "Vendor"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
|
||||
|
||||
;; Memo field
|
||||
|
||||
;; Approval status field
|
||||
(manual-coding-section* (manual-mode-initial snapshot) request)
|
||||
(fc/with-field :transaction/approval-status
|
||||
(com/validated-field
|
||||
{:label "Status"
|
||||
@@ -867,12 +972,7 @@
|
||||
(com/button-group-button {"@click" "approvalStatus = 'suppressed'"
|
||||
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }"
|
||||
:class "rounded-r-lg"}
|
||||
"Client Review")]])))
|
||||
(fc/with-field :transaction/accounts
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)}
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)]))]]]])
|
||||
"Client Review")]])))]]]])
|
||||
:footer
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate
|
||||
:next-button (com/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Done"))
|
||||
@@ -1329,9 +1429,11 @@
|
||||
(let [multi-form-state (:multi-form-state request)
|
||||
snapshot (:snapshot multi-form-state)
|
||||
step-params (:step-params multi-form-state)
|
||||
mode (keyword (or (:mode step-params) "simple"))
|
||||
client-id (or (:transaction/client snapshot)
|
||||
(-> request :entity :transaction/client :db/id))
|
||||
vendor-id (or (:transaction/vendor step-params) (:transaction/vendor snapshot))
|
||||
vendor-id (or (:transaction/vendor step-params)
|
||||
(:transaction/vendor snapshot))
|
||||
total (Math/abs (or (-> request :entity :transaction/amount)
|
||||
(:transaction/amount snapshot)
|
||||
0.0))
|
||||
@@ -1351,8 +1453,39 @@
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
|
||||
request)]
|
||||
(html-response
|
||||
[:div#account-grid-body
|
||||
(render-account-grid-body render-request)])))
|
||||
(fc/start-form (:multi-form-state render-request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* mode render-request))))))
|
||||
|
||||
(defn edit-wizard-toggle-mode-handler [request]
|
||||
(let [step-params (-> request :multi-form-state :step-params)
|
||||
snapshot (-> request :multi-form-state :snapshot)
|
||||
current-mode (keyword (or (:mode step-params) "simple"))
|
||||
target-mode (if (= current-mode :simple) :advanced :simple)
|
||||
;; When switching simple→advanced, promote simple-mode values into accounts
|
||||
render-request
|
||||
(if (and (= target-mode :advanced)
|
||||
(= current-mode :simple))
|
||||
;; carry the simple-mode single row into snapshot so the table shows it
|
||||
(let [accounts (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot)))]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
|
||||
(vec accounts))
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts]
|
||||
(vec accounts))))
|
||||
;; advanced→simple: take first row only
|
||||
(let [first-row (first (or (seq (:transaction/accounts step-params))
|
||||
(seq (:transaction/accounts snapshot))))]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
|
||||
(if first-row [first-row] []))
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts]
|
||||
(if first-row [first-row] [])))))]
|
||||
(html-response
|
||||
(fc/start-form (:multi-form-state render-request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* target-mode render-request))))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
@@ -1395,6 +1528,10 @@
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-toggle-mode (-> edit-wizard-toggle-mode-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-new-account (->
|
||||
(add-new-entity-handler [:step-params :transaction/accounts]
|
||||
(fn render [cursor request]
|
||||
|
||||
32
src/clj/dev_mcp.clj
Normal file
32
src/clj/dev_mcp.clj
Normal file
@@ -0,0 +1,32 @@
|
||||
(ns dev-mcp
|
||||
"Leiningen task: start nREPL + web server on random ports."
|
||||
(:require [clojure.java.io :as io]
|
||||
[nrepl.server :as nrepl])
|
||||
(:import [java.net ServerSocket]))
|
||||
|
||||
(defn available-port []
|
||||
"Pick a random available TCP port."
|
||||
(with-open [s (ServerSocket. 0)]
|
||||
(.getLocalPort s)))
|
||||
|
||||
(defn- mcp-repl-task [& _args]
|
||||
"Start nREPL server and HTTP server on random ports.
|
||||
|
||||
Writes ports to nrepl-port and .http-port files.
|
||||
Connect with: clj-nrepl-eval -p $(cat nrepl-port)"
|
||||
(let [nrepl-port (available-port)
|
||||
http-port (available-port)]
|
||||
(spit "nrepl-port" (str nrepl-port))
|
||||
(spit ".http-port" (str http-port))
|
||||
(println (format "nREPL port: %d (nrepl-port)" nrepl-port))
|
||||
(println (format "HTTP port: %d (.http-port)" http-port))
|
||||
(nrepl/start-server :port nrepl-port)
|
||||
(require 'user)
|
||||
((resolve 'user/start-dev) http-port)
|
||||
(println "Ready.")
|
||||
@(promise)))
|
||||
|
||||
(defn -main
|
||||
"Entry point for: lein trampoline run -m dev-mcp"
|
||||
[& args]
|
||||
(apply mcp-repl-task args))
|
||||
105
src/clj/user.clj
105
src/clj/user.clj
@@ -84,24 +84,24 @@
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn load-accounts [conn]
|
||||
(let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv)
|
||||
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
|
||||
:db/id])]
|
||||
:in ['$]
|
||||
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
|
||||
:db/id])]
|
||||
:in ['$]
|
||||
:where ['[?e :account/name]]}
|
||||
(dc/db conn))))
|
||||
|
||||
also-merge-txes (fn [also-merge old-account-id]
|
||||
(if old-account-id
|
||||
(let [[sunset-account]
|
||||
(first (dc/q {:find ['?a]
|
||||
:in ['$ '?ac]
|
||||
(first (dc/q {:find ['?a]
|
||||
:in ['$ '?ac]
|
||||
:where ['[?a :account/numeric-code ?ac]]}
|
||||
(dc/db conn) also-merge))]
|
||||
(into (mapv
|
||||
(fn [[entity id _]]
|
||||
[:db/add entity id old-account-id])
|
||||
(dc/q {:find ['?e '?id '?a]
|
||||
:in ['$ '?ac]
|
||||
(dc/q {:find ['?e '?id '?a]
|
||||
:in ['$ '?ac]
|
||||
:where ['[?a :account/numeric-code ?ac]
|
||||
'[?e ?at ?a]
|
||||
'[?at :db/ident ?id]]}
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
txes (transduce
|
||||
(comp
|
||||
(map (fn ->map [r]
|
||||
(map (fn ->map [r]
|
||||
(into {} (map vector header r))))
|
||||
(map (fn parse-map [r]
|
||||
{:old-account-id (:db/id (code->existing-account
|
||||
@@ -160,8 +160,8 @@
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn find-bad-accounts []
|
||||
(set (map second (dc/q {:find ['(pull ?x [*]) '?z]
|
||||
:in ['$]
|
||||
(set (map second (dc/q {:find ['(pull ?x [*]) '?z]
|
||||
:in ['$]
|
||||
:where ['[?e :account/numeric-code ?z]
|
||||
'[(<= ?z 9999)]
|
||||
'[?x ?a ?e]]}
|
||||
@@ -177,8 +177,8 @@
|
||||
[:db/retractEntity old-account-id])))
|
||||
conj
|
||||
[]
|
||||
(dc/q {:find ['?e]
|
||||
:in ['$]
|
||||
(dc/q {:find ['?e]
|
||||
:in ['$]
|
||||
:where ['[?e :account/numeric-code ?z]
|
||||
'[(<= ?z 9999)]]}
|
||||
(dc/db conn)))))
|
||||
@@ -192,27 +192,27 @@
|
||||
(fn [acc [e z]]
|
||||
(update acc z conj e))
|
||||
{}
|
||||
(dc/q {:find ['?e '?z]
|
||||
:in ['$]
|
||||
(dc/q {:find ['?e '?z]
|
||||
:in ['$]
|
||||
:where ['[?e :account/numeric-code ?z]]}
|
||||
(dc/db conn)))))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn customize-accounts [customer filename]
|
||||
(let [[_ & rows] (-> filename (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv)
|
||||
[client-id] (first (dc/q (-> {:find ['?e]
|
||||
:in ['$ '?z]
|
||||
[client-id] (first (dc/q (-> {:find ['?e]
|
||||
:in ['$ '?z]
|
||||
:where [['?e :client/code '?z]]}
|
||||
(dc/db conn) customer)))
|
||||
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
|
||||
{:account/applicability [:db/ident]}
|
||||
:db/id])]
|
||||
:in ['$]
|
||||
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
|
||||
{:account/applicability [:db/ident]}
|
||||
:db/id])]
|
||||
:in ['$]
|
||||
:where ['[?e :account/name]]}
|
||||
(dc/db conn))))
|
||||
|
||||
existing-account-overrides (dc/q {:find ['?e]
|
||||
:in ['$ '?client-id]
|
||||
existing-account-overrides (dc/q {:find ['?e]
|
||||
:in ['$ '?client-id]
|
||||
:where [['?e :account-client-override/client '?client-id]]}
|
||||
(dc/db conn) client-id)
|
||||
|
||||
@@ -276,8 +276,8 @@
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn fix-transactions-without-locations [client-code location]
|
||||
(->>
|
||||
(dc/q {:find ['(pull ?e [*])]
|
||||
:in ['$ '?client-code]
|
||||
(dc/q {:find ['(pull ?e [*])]
|
||||
:in ['$ '?client-code]
|
||||
:where ['[?e :transaction/accounts ?ta]
|
||||
'[?e :transaction/matched-rule]
|
||||
'[?e :transaction/approval-status :transaction-approval-status/approved]
|
||||
@@ -297,8 +297,8 @@
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn entity-history [i]
|
||||
(vec (sort-by first (dc/q
|
||||
{:find ['?tx '?z '?v]
|
||||
:in ['?i '$]
|
||||
{:find ['?tx '?z '?v]
|
||||
:in ['?i '$]
|
||||
:where ['[?i ?a ?v ?tx ?ad]
|
||||
'[?a :db/ident ?z]
|
||||
'[(= ?ad true)]]}
|
||||
@@ -307,8 +307,8 @@
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn entity-history-with-revert [i]
|
||||
(vec (sort-by first (dc/q
|
||||
{:find ['?tx '?z '?v '?ad]
|
||||
:in ['?i '$]
|
||||
{:find ['?tx '?z '?v '?ad]
|
||||
:in ['?i '$]
|
||||
:where ['[?i ?a ?v ?tx ?ad]
|
||||
'[?a :db/ident ?z]]}
|
||||
i (dc/history (dc/db conn))))))
|
||||
@@ -347,15 +347,18 @@
|
||||
(hawk.core/watch! [{:paths ["src/" "test/"]
|
||||
:handler auto-reset-handler}]))
|
||||
|
||||
(defn start-http []
|
||||
(defn start-http [& [http-port]]
|
||||
(when http-port
|
||||
(alter-var-root #'auto-ap.server/*http-port-override* (constantly http-port)))
|
||||
(mount.core/start (mount.core/only #{#'auto-ap.server/port #'auto-ap.server/jetty})))
|
||||
|
||||
(defn start-dev []
|
||||
(defn start-dev [& [http-port]]
|
||||
(set-refresh-dirs "src")
|
||||
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.server))
|
||||
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.time))
|
||||
(clojure.tools.namespace.repl/disable-reload! (find-ns 'dev-mcp))
|
||||
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.server))
|
||||
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.time))
|
||||
(start-db)
|
||||
(start-http)
|
||||
(start-http http-port)
|
||||
(auto-reset))
|
||||
|
||||
#_(defn start-search []
|
||||
@@ -376,18 +379,18 @@
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn find-queries [words]
|
||||
(let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env)
|
||||
:prefix (str "queries/"))
|
||||
concurrent 30
|
||||
(let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env)
|
||||
:prefix (str "queries/"))
|
||||
concurrent 30
|
||||
output-chan (async/chan)]
|
||||
(async/pipeline-blocking concurrent
|
||||
output-chan
|
||||
(comp
|
||||
(map #(do
|
||||
[(:key %)
|
||||
(str (slurp (:object-content (s3/get-object
|
||||
:bucket-name (:data-bucket env)
|
||||
:key (:key %)))))]))
|
||||
(str (slurp (:object-content (s3/get-object
|
||||
:bucket-name (:data-bucket env)
|
||||
:key (:key %)))))]))
|
||||
|
||||
(filter #(->> words
|
||||
(every? (fn [w] (str/includes? (second %) w)))))
|
||||
@@ -401,9 +404,9 @@
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn upsert-invoice-amounts [tsv]
|
||||
(let [data (with-open [reader (io/reader (char-array tsv))]
|
||||
(doall (csv/read-csv reader :separator \tab)))
|
||||
db (dc/db conn)
|
||||
(let [data (with-open [reader (io/reader (char-array tsv))]
|
||||
(doall (csv/read-csv reader :separator \tab)))
|
||||
db (dc/db conn)
|
||||
i->invoice-id (fn [i]
|
||||
(try (Long/parseLong i)
|
||||
(catch Exception e
|
||||
@@ -456,7 +459,7 @@
|
||||
:when current-total]
|
||||
|
||||
[(when (not (auto-ap.utils/dollars= current-total target-total))
|
||||
{:db/id invoice-id
|
||||
{:db/id invoice-id
|
||||
:invoice/total target-total})
|
||||
|
||||
(when new-account?
|
||||
@@ -521,7 +524,7 @@
|
||||
(let [client-location (ffirst (d/q '[:find ?l :in $ ?c :where [?c :client/locations ?l]] (dc/db conn) [:client/code client-code]))]
|
||||
(clojure.data.csv/write-csv
|
||||
*out*
|
||||
(for [n (range n)
|
||||
(for [n (range n)
|
||||
:let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn))))
|
||||
[{a-1 :account/numeric-code a-1-location :account/location}
|
||||
{a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]]
|
||||
@@ -534,8 +537,8 @@
|
||||
(t/minus (t/days (rand-int 60)))
|
||||
(atime/unparse atime/normal-date))
|
||||
id (rand-int 100000)]
|
||||
a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount]
|
||||
[(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]]
|
||||
a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount]
|
||||
[(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]]
|
||||
a)
|
||||
:separator \tab))))
|
||||
|
||||
@@ -547,7 +550,7 @@
|
||||
(let [bank-accounts (map first (d/q '[:find ?bac :in $ ?c :where [?c :client/bank-accounts ?b] [?b :bank-account/code ?bac]] (dc/db conn) [:client/code client-code]))]
|
||||
(clojure.data.csv/write-csv
|
||||
*out*
|
||||
(for [n (range n)
|
||||
(for [n (range n)
|
||||
:let [amount (rand-int 2000)
|
||||
d (-> (t/now)
|
||||
(t/minus (t/days (rand-int 60)))
|
||||
@@ -563,7 +566,7 @@
|
||||
:in $
|
||||
:where [?i :invoice/invoice-number]
|
||||
(not [?i :invoice/status :invoice-status/voided])]
|
||||
:args [(dc/db conn)]})
|
||||
:args [(dc/db conn)]})
|
||||
(map first)
|
||||
(partition-all 500))]
|
||||
(print ".")
|
||||
@@ -576,7 +579,7 @@
|
||||
:in $
|
||||
:where [?i :payment/date]
|
||||
(not [?i :payment/status :payment-status/voided])]
|
||||
:args [(dc/db conn)]})
|
||||
:args [(dc/db conn)]})
|
||||
(map first)
|
||||
(partition-all 500))]
|
||||
(print ".")
|
||||
@@ -589,7 +592,7 @@
|
||||
:in $
|
||||
:where [?i :transaction/description-original]
|
||||
(not [?i :transaction/approval-status :transaction-approval-status/suppressed])]
|
||||
:args [(dc/db conn)]})
|
||||
:args [(dc/db conn)]})
|
||||
(map first)
|
||||
(partition-all 500))]
|
||||
(print ".")
|
||||
@@ -600,7 +603,7 @@
|
||||
(doseq [batch (->> (dc/qseq {:query '[:find ?i
|
||||
:in $
|
||||
:where [?i :journal-entry/date]]
|
||||
:args [(dc/db conn)]})
|
||||
:args [(dc/db conn)]})
|
||||
(map first)
|
||||
(partition-all 500))]
|
||||
(print ".")
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
(ns auto-ap.routes.transactions)
|
||||
|
||||
(def routes {"" {:get ::page
|
||||
:put ::edit-wizard-navigate
|
||||
"/unapproved" ::unapproved-page
|
||||
"/requires-feedback" ::requires-feedback-page
|
||||
"/approved" ::approved-page
|
||||
"/bulk-delete" ::bulk-delete
|
||||
"/bulk-suppress" ::bulk-suppress
|
||||
"/bulk-code" {:get ::bulk-code
|
||||
:put ::bulk-code-submit
|
||||
"/new-account" ::bulk-code-new-account
|
||||
"/vendor-changed" ::bulk-code-vendor-changed}}
|
||||
(def routes {"" {:get ::page
|
||||
:put ::edit-wizard-navigate
|
||||
"/unapproved" ::unapproved-page
|
||||
"/requires-feedback" ::requires-feedback-page
|
||||
"/approved" ::approved-page
|
||||
"/bulk-delete" ::bulk-delete
|
||||
"/bulk-suppress" ::bulk-suppress
|
||||
"/bulk-code" {:get ::bulk-code
|
||||
:put ::bulk-code-submit
|
||||
"/new-account" ::bulk-code-new-account
|
||||
"/vendor-changed" ::bulk-code-vendor-changed}
|
||||
"/bulk-code-trial" {:get ::bulk-code-trial
|
||||
:post ::bulk-code-trial-submit}}
|
||||
"/new" {:get ::new
|
||||
:post ::new-submit
|
||||
"/location-select" ::location-select
|
||||
@@ -22,9 +24,9 @@
|
||||
"/parse" ::external-import-parse
|
||||
"/import" ::external-import-import}
|
||||
|
||||
"/table" ::table
|
||||
"/csv" ::csv
|
||||
"/bank-account-filter" ::bank-account-filter
|
||||
"/table" ::table
|
||||
"/csv" ::csv
|
||||
"/bank-account-filter" ::bank-account-filter
|
||||
|
||||
["/" [#"\d+" :db/id]] {"/edit" {:get ::edit-wizard}}
|
||||
"/edit-submit" ::edit-submit
|
||||
@@ -34,6 +36,7 @@
|
||||
"/account-balance" ::account-balance
|
||||
"/toggle-amount-mode" ::toggle-amount-mode
|
||||
"/edit-wizard-new-account" ::edit-wizard-new-account
|
||||
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
|
||||
"/match-payment" ::link-payment
|
||||
"/match-autopay-invoices" ::link-autopay-invoices
|
||||
"/match-unpaid-invoices" ::link-unpaid-invoices
|
||||
|
||||
@@ -44,10 +44,10 @@ module.exports = {
|
||||
"gg": "gentleGrow 1s infinite",
|
||||
"slideUp": 'slideUp 0.5s ease-out forwards'
|
||||
},
|
||||
"fontFamily": {
|
||||
"sans": ["Inter", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"],
|
||||
"mono": ["JetBrains Mono", "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "monospace"]
|
||||
},
|
||||
"fontFamily": {
|
||||
"sans": ["Calibri", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"]
|
||||
|
||||
},
|
||||
"colors": {
|
||||
"green": {
|
||||
"50": "#f2f8ea",
|
||||
|
||||
114
test/clj/auto_ap/ssr/components/wizard_trial/core_test.clj
Normal file
114
test/clj/auto_ap/ssr/components/wizard_trial/core_test.clj
Normal file
@@ -0,0 +1,114 @@
|
||||
(ns auto-ap.ssr.components.wizard-trial.core-test
|
||||
(:require
|
||||
[auto-ap.ssr.components.wizard-trial.core :as sut]
|
||||
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[hiccup.core :as hiccup]))
|
||||
|
||||
(deftest render-step-test
|
||||
(testing "render-step produces a form with hidden wizard-id and step-key inputs"
|
||||
(let [wizard-id (ws/create! {})
|
||||
step-config {:key :test-step
|
||||
:render (fn [{:keys [step-data errors request]}]
|
||||
[:div (str "data: " step-data) (when errors [:span.error (str errors)])])
|
||||
:submit-route "/test-submit"}
|
||||
result (sut/render-step {:wizard-id wizard-id
|
||||
:step-config step-config
|
||||
:request {}})]
|
||||
(is (= :form (first result)))
|
||||
(let [attrs (second result)
|
||||
children (drop 2 result)]
|
||||
(is (= "/test-submit" (:hx-post attrs)))
|
||||
(is (= "this" (:hx-target attrs)))
|
||||
(let [html (hiccup/html result)]
|
||||
(is (re-find #"name=\"wizard-id\"" html))
|
||||
(is (re-find #"value=\"" html))
|
||||
(is (re-find #"name=\"step-key\"" html))
|
||||
(is (re-find #"value=\"test-step\"" html))))))
|
||||
|
||||
(testing "render-step passes step-data, errors, and request to the render function"
|
||||
(let [wizard-id (ws/create! {:test-step {:field "value"}})
|
||||
captured (atom nil)
|
||||
step-config {:key :test-step
|
||||
:render (fn [args] (reset! captured args) [:div "rendered"])
|
||||
:submit-route "/test-submit"}
|
||||
_ (sut/render-step {:wizard-id wizard-id
|
||||
:step-config step-config
|
||||
:request {:client-id 123}
|
||||
:errors {:field ["is invalid"]}})
|
||||
{:keys [step-data errors request]} @captured]
|
||||
(is (= {:field "value"} step-data))
|
||||
(is (= {:field ["is invalid"]} errors))
|
||||
(is (= {:client-id 123} request)))))
|
||||
|
||||
(deftest handle-submit-test
|
||||
(testing "handle-submit with valid data saves step and calls done-fn"
|
||||
(let [done-result (atom nil)
|
||||
wizard-id (ws/create! {})
|
||||
step-config {:key :test-step
|
||||
:schema [:map [:name :string]]
|
||||
:fields [:name]
|
||||
:render (fn [_] [:div "rendered"])
|
||||
:submit-route "/test-submit"
|
||||
:done-fn (fn [data request]
|
||||
(reset! done-result {:data data :request request})
|
||||
{:status 200 :body "done"})}
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "test-step"
|
||||
"name" "Alice"}}
|
||||
response (sut/handle-submit step-config request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= "done" (:body response)))
|
||||
(is (= "Alice" (get-in @done-result [:data :name])))
|
||||
(is (nil? (ws/get-wizard wizard-id)) "Wizard session should be destroyed after successful submit")))
|
||||
|
||||
(testing "handle-submit with invalid data re-renders step with errors"
|
||||
(let [wizard-id (ws/create! {})
|
||||
step-config {:key :test-step
|
||||
:schema [:map [:name :string]]
|
||||
:fields [:name]
|
||||
:render (fn [{:keys [errors]}]
|
||||
[:div (when errors [:span.error (str errors)])])
|
||||
:submit-route "/test-submit"
|
||||
:done-fn (fn [_ _] {:status 200 :body "done"})}
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "test-step"
|
||||
"name" ""}}
|
||||
response (sut/handle-submit step-config request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (string? (:body response)))
|
||||
(is (re-find #"error" (:body response)) "Response body should contain error markup")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should still exist after failed validation")))
|
||||
|
||||
(testing "handle-submit with missing required field shows validation error"
|
||||
(let [wizard-id (ws/create! {})
|
||||
step-config {:key :test-step
|
||||
:schema [:map [:name :string]]
|
||||
:fields [:name]
|
||||
:render (fn [{:keys [errors]}]
|
||||
[:div (when errors [:span.error (str errors)])])
|
||||
:submit-route "/test-submit"
|
||||
:done-fn (fn [_ _] {:status 200 :body "done"})}
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "test-step"}}
|
||||
response (sut/handle-submit step-config request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (re-find #"error" (:body response)) "Response body should contain error markup for missing field")))
|
||||
|
||||
(testing "handle-submit decodes step data using main-transformer"
|
||||
(let [done-result (atom nil)
|
||||
wizard-id (ws/create! {})
|
||||
step-config {:key :test-step
|
||||
:schema [:map [:count int?]]
|
||||
:fields [:count]
|
||||
:render (fn [_] [:div "rendered"])
|
||||
:submit-route "/test-submit"
|
||||
:done-fn (fn [data _]
|
||||
(reset! done-result data)
|
||||
{:status 200 :body "done"})}
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "test-step"
|
||||
"count" "42"}}
|
||||
response (sut/handle-submit step-config request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= 42 (:count @done-result)) "String count should be decoded to integer"))))
|
||||
76
test/clj/auto_ap/ssr/components/wizard_trial/state_test.clj
Normal file
76
test/clj/auto_ap/ssr/components/wizard_trial/state_test.clj
Normal file
@@ -0,0 +1,76 @@
|
||||
(ns auto-ap.ssr.components.wizard-trial.state-test
|
||||
(:require
|
||||
[auto-ap.ssr.components.wizard-trial.state :as sut]
|
||||
[clojure.test :refer [deftest is testing]]))
|
||||
|
||||
(deftest create-and-get-wizard-test
|
||||
(testing "Session creation returns a non-nil wizard-id"
|
||||
(let [wizard-id (sut/create! {:foo "bar"})]
|
||||
(is (string? wizard-id))
|
||||
(is (seq wizard-id))))
|
||||
|
||||
(testing "Session retrieval returns the stored data"
|
||||
(let [wizard-id (sut/create! {:foo "bar"})
|
||||
wizard (sut/get-wizard wizard-id)]
|
||||
(is (map? wizard))
|
||||
(is (= {:foo "bar"} (:data wizard)))
|
||||
(is (inst? (:created-at wizard)))))
|
||||
|
||||
(testing "Session retrieval returns nil for unknown id"
|
||||
(is (nil? (sut/get-wizard "non-existent-id")))))
|
||||
|
||||
(deftest update-step-test
|
||||
(testing "update-step! merges data into the specified step key"
|
||||
(let [wizard-id (sut/create! {})
|
||||
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||
wizard (sut/get-wizard wizard-id)]
|
||||
(is (= {:field-a "a"} (get-in wizard [:data :step1])))))
|
||||
|
||||
(testing "update-step! merges without overwriting other step keys"
|
||||
(let [wizard-id (sut/create! {})
|
||||
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||
_ (sut/update-step! wizard-id :step2 {:field-b "b"})
|
||||
wizard (sut/get-wizard wizard-id)]
|
||||
(is (= {:field-a "a"} (get-in wizard [:data :step1])))
|
||||
(is (= {:field-b "b"} (get-in wizard [:data :step2])))))
|
||||
|
||||
(testing "update-step! merges within the same step key"
|
||||
(let [wizard-id (sut/create! {})
|
||||
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||
_ (sut/update-step! wizard-id :step1 {:field-b "b"})
|
||||
wizard (sut/get-wizard wizard-id)]
|
||||
(is (= {:field-a "a" :field-b "b"} (get-in wizard [:data :step1]))))))
|
||||
|
||||
(deftest destroy-test
|
||||
(testing "destroy! removes the wizard session"
|
||||
(let [wizard-id (sut/create! {:foo "bar"})
|
||||
_ (sut/destroy! wizard-id)]
|
||||
(is (nil? (sut/get-wizard wizard-id)))))
|
||||
|
||||
(testing "destroy! is a no-op for unknown id"
|
||||
(sut/destroy! "non-existent-id")
|
||||
(is (nil? (sut/get-wizard "non-existent-id")))))
|
||||
|
||||
(deftest get-all-data-test
|
||||
(testing "get-all-data merges non-map values and map values from all steps"
|
||||
(let [wizard-id (sut/create! {:client-id 123})
|
||||
_ (sut/update-step! wizard-id :step1 {:vendor 456})
|
||||
_ (sut/update-step! wizard-id :step2 {:accounts [{:account 1}]})
|
||||
all-data (sut/get-all-data wizard-id)]
|
||||
(is (= {:client-id 123 :vendor 456 :accounts [{:account 1}]} all-data))))
|
||||
|
||||
(testing "get-all-data returns nil for unknown id"
|
||||
(is (nil? (sut/get-all-data "non-existent-id")))))
|
||||
|
||||
(deftest session-exists-test
|
||||
(testing "Session exists after creation"
|
||||
(let [wizard-id (sut/create! {})]
|
||||
(is (some? (sut/get-wizard wizard-id)))))
|
||||
|
||||
(testing "Session does not exist after destruction"
|
||||
(let [wizard-id (sut/create! {})
|
||||
_ (sut/destroy! wizard-id)]
|
||||
(is (nil? (sut/get-wizard wizard-id)))))
|
||||
|
||||
(testing "Session does not exist for random id"
|
||||
(is (nil? (sut/get-wizard (str (java.util.UUID/randomUUID)))))))
|
||||
104
test/clj/auto_ap/ssr/transaction/bulk_code_trial_test.clj
Normal file
104
test/clj/auto_ap/ssr/transaction/bulk_code_trial_test.clj
Normal file
@@ -0,0 +1,104 @@
|
||||
(ns auto-ap.ssr.transaction.bulk-code-trial-test
|
||||
(:require
|
||||
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||
[auto-ap.ssr.transaction.bulk-code-trial :as sut]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(use-fixtures :each
|
||||
(fn [test-fn]
|
||||
(mount/start #'auto-ap.datomic/conn)
|
||||
(test-fn)
|
||||
(mount/stop #'auto-ap.datomic/conn)))
|
||||
|
||||
(deftest open-trial-test
|
||||
(testing "open-trial returns modal-response with a form containing expected fields"
|
||||
(let [response (sut/open-trial {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= "text/html" (get-in response [:headers "Content-Type"])))
|
||||
(let [body (:body response)]
|
||||
(is (string? body))
|
||||
(is (re-find #"modal-card" body) "Should contain modal card structure")
|
||||
(is (re-find #"Bulk editing" body) "Should show header with transaction count")
|
||||
(is (re-find #"Vendor" body) "Form should contain Vendor label")
|
||||
(is (re-find #"Status" body) "Form should contain Status label")
|
||||
(is (re-find #"Expense Accounts" body) "Form should contain Expense Accounts heading")
|
||||
(is (re-find #"Account" body) "Form should contain Account column header")
|
||||
(is (re-find #"Location" body) "Form should contain Location column header")
|
||||
(is (re-find #"%" body) "Form should contain percentage column header")
|
||||
(is (re-find #"Save" body) "Form should contain Save button")
|
||||
(is (re-find #"New account" body) "Form should contain New account button")
|
||||
(is (re-find #"name=\"wizard-id\"" body) "Form should contain hidden wizard-id input")
|
||||
(is (re-find #"name=\"step-key\"" body) "Form should contain hidden step-key input")))))
|
||||
|
||||
(deftest submit-trial-valid-test
|
||||
(testing "submit-trial with valid data returns success response and destroys session"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"
|
||||
"vendor" "123"
|
||||
"approval-status" "approved"
|
||||
"accounts[0][account]" "1"
|
||||
"accounts[0][location]" "DT"
|
||||
"accounts[0][percentage]" "50"}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (re-find #"Transactions Coded" (:body response)) "Response should indicate success")
|
||||
(is (nil? (ws/get-wizard wizard-id)) "Wizard session should be destroyed after successful submit"))))
|
||||
|
||||
(deftest submit-trial-invalid-test
|
||||
(testing "submit-trial with invalid vendor id shows validation error"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"
|
||||
"vendor" "not-a-number"
|
||||
"approval-status" "approved"}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (re-find #"error" (:body response)) "Response should contain error markup for invalid vendor")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||
|
||||
(testing "submit-trial with invalid account data shows validation error"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"
|
||||
"accounts[0][account]" "not-a-number"
|
||||
"accounts[0][location]" "DT"
|
||||
"accounts[0][percentage]" "50"}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (re-find #"error" (:body response)) "Response should contain error markup for invalid account")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||
|
||||
(testing "submit-trial with percentage over 100% shows validation error"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"
|
||||
"accounts[0][account]" "1"
|
||||
"accounts[0][location]" "DT"
|
||||
"accounts[0][percentage]" "150"}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (re-find #"error" (:body response)) "Response should contain error markup for percentage > 100%")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation"))))
|
||||
|
||||
(deftest submit-trial-empty-test
|
||||
(testing "submit-trial with empty form data shows validation errors"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (not (re-find #"Bulk code applied" (:body response))) "Empty form should not succeed")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||
|
||||
(testing "submit-trial with no account rows selected shows validation errors"
|
||||
(let [wizard-id (ws/create! {})
|
||||
request {:form-params {"wizard-id" wizard-id
|
||||
"step-key" "bulk-code"
|
||||
"vendor" ""
|
||||
"approval-status" ""}}
|
||||
response (sut/submit-trial request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (not (re-find #"Bulk code applied" (:body response))) "Form with empty values should not succeed")
|
||||
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation"))))
|
||||
@@ -0,0 +1,936 @@
|
||||
(ns auto-ap.ssr.transaction.edit-simple-advanced-mode-test
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.integration.util :refer [wrap-setup]]
|
||||
[auto-ap.solr]
|
||||
[auto-ap.ssr.components.multi-modal :as mm]
|
||||
[auto-ap.ssr.form-cursor :as fc]
|
||||
[auto-ap.ssr.transaction.edit :refer [clientize-vendor
|
||||
edit-vendor-changed-handler
|
||||
edit-wizard-toggle-mode-handler
|
||||
location-select*
|
||||
manual-coding-section*
|
||||
vendor-default-account]]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup.core :as hiccup]))
|
||||
|
||||
;; Private save-handler accessible via var reference
|
||||
(def ^:private save-handler
|
||||
#'auto-ap.ssr.transaction.edit/save-handler)
|
||||
|
||||
(use-fixtures :each wrap-setup)
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Helpers
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- tempid->id
|
||||
"Resolve a string temp ID to its numeric Datomic entity ID after transact."
|
||||
[result temp-id]
|
||||
(get (:tempids result) temp-id))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC1-3: manual-mode-initial — mode selection on open
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
;; manual-mode-initial is private; access via var reference
|
||||
(def ^:private manual-mode-initial
|
||||
#'auto-ap.ssr.transaction.edit/manual-mode-initial)
|
||||
|
||||
(deftest manual-mode-initial-test
|
||||
(testing "AC1: uncoded transaction (no accounts) opens in simple mode"
|
||||
(is (= :simple (manual-mode-initial {:db/id 123})))
|
||||
(is (= :simple (manual-mode-initial {:db/id 123 :transaction/accounts []}))))
|
||||
|
||||
(testing "AC2: single-account transaction opens in simple mode"
|
||||
(is (= :simple (manual-mode-initial {:db/id 123
|
||||
:transaction/accounts [{:transaction-account/account 456
|
||||
:transaction-account/location "Shared"
|
||||
:transaction-account/amount 100.0}]}))))
|
||||
|
||||
(testing "AC3: multi-account (2+) transaction opens in advanced mode"
|
||||
(is (= :advanced (manual-mode-initial {:db/id 123
|
||||
:transaction/accounts [{:transaction-account/account 1}
|
||||
{:transaction-account/account 2}]})))
|
||||
(is (= :advanced (manual-mode-initial {:db/id 123
|
||||
:transaction/accounts [{} {} {}]})))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC4-5: edit-vendor-changed-handler — vendor selection in simple mode
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest edit-vendor-changed-simple-mode-test
|
||||
(testing "AC4: vendor selection in simple mode populates account/location from vendor default"
|
||||
;; Set up vendor with a default account
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "Test Vendor"}
|
||||
{:db/id "account-id"
|
||||
:account/name "Test Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/default-account "account-id"}
|
||||
{:db/id "client-id"
|
||||
:client/code "TESTCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts []}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/accounts []})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-vendor-changed-handler request)
|
||||
body (:body response)]
|
||||
;; Response should contain the manual-coding-section div
|
||||
(is (re-find #"manual-coding-section" body)
|
||||
"Response should contain the manual-coding-section element")
|
||||
;; The vendor's default account ID should appear in the rendered HTML
|
||||
(is (re-find (re-pattern (str account-id)) body)
|
||||
"Response should contain the vendor's default account ID")
|
||||
;; The account's name should appear in the rendered HTML
|
||||
(is (re-find #"Test Account" body)
|
||||
"Response should contain the vendor's default account name")))
|
||||
|
||||
(testing "AC5: vendor selection in simple mode does NOT overwrite already-set account"
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "Test Vendor"}
|
||||
{:db/id "account-id"
|
||||
:account/name "Test Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/default-account "account-id"}
|
||||
{:db/id "other-account-id"
|
||||
:account/name "Other Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-id"
|
||||
:client/code "TESTCL2"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
other-account-id (tempid->id result "other-account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
;; existing-accounts already set means vendor should NOT overwrite
|
||||
existing-accounts [{:db/id "row-id"
|
||||
:transaction-account/account other-account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts existing-accounts}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/accounts existing-accounts})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-vendor-changed-handler request)
|
||||
body (:body response)]
|
||||
;; The handler returns an html-response; verify the body is HTML
|
||||
(is (re-find #"manual-coding-section" body)
|
||||
"Response body should contain the manual-coding-section element")
|
||||
;; The original account ID must still appear in the rendered HTML
|
||||
(is (re-find (re-pattern (str other-account-id)) body)
|
||||
"Response should contain the original (pre-existing) account ID")
|
||||
;; The vendor's default account ID must NOT appear — it was not used
|
||||
(is (not (re-find (re-pattern (str (tempid->id result "account-id"))) body))
|
||||
"Response should NOT contain the vendor's default account ID when existing account is set"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC6: save round-trip — manual mode saves vendor + account to DB
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest save-manual-round-trip-test
|
||||
(testing "AC6: save in simple mode persists vendor/account/location — re-opening shows same values"
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "Save Vendor"}
|
||||
{:db/id "account-id"
|
||||
:account/name "Save Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-id"
|
||||
:client/code "SAVECL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
new-row-id (str (java.util.UUID/randomUUID))
|
||||
snapshot {:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:action :manual
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/amount 100.0
|
||||
:transaction/accounts [{:db/id new-row-id
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "Shared"
|
||||
:transaction-account/amount 100.0}]}
|
||||
request {:multi-form-state (mm/->MultiStepFormState snapshot [] snapshot)
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}
|
||||
:identity {:user/role "admin"}}]
|
||||
(with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))]
|
||||
(save-handler request))
|
||||
;; Verify the transaction was saved to DB with vendor and accounts
|
||||
(let [saved (dc/pull (dc/db conn)
|
||||
'[:db/id
|
||||
{:transaction/vendor [:db/id]}
|
||||
{:transaction/accounts [{:transaction-account/account [:db/id]}
|
||||
:transaction-account/location
|
||||
:transaction-account/amount]}]
|
||||
tx-id)]
|
||||
(is (= vendor-id (-> saved :transaction/vendor :db/id))
|
||||
"Vendor should be saved on the transaction")
|
||||
(is (= 1 (count (:transaction/accounts saved)))
|
||||
"Exactly one account row should be saved")
|
||||
(is (= account-id (-> saved :transaction/accounts first :transaction-account/account :db/id))
|
||||
"The correct account should be saved")
|
||||
;; "Shared" is spread to client-specific locations by maybe-spread-locations
|
||||
(is (= "DT" (-> saved :transaction/accounts first :transaction-account/location))
|
||||
"The location should be saved (Shared spreads to client locations)")
|
||||
(is (= 100.0 (-> saved :transaction/accounts first :transaction-account/amount))
|
||||
"The amount should be saved")))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC7: simple mode account typeahead respects :account/default-allowance rules
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest simple-mode-account-typeahead-allowance-test
|
||||
(testing "AC7: simple mode account typeahead URL includes purpose=transaction"
|
||||
;; The account-typeahead* always passes purpose=transaction to the search URL,
|
||||
;; and the schema validates accounts using check-allowance with :account/default-allowance.
|
||||
;; We test the rendering side: the typeahead URL in simple-mode-fields* must contain purpose=transaction.
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "ALLOWCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Allowed Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
one-account [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts one-account}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/accounts one-account})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
html (hiccup/html
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :simple request))))]
|
||||
(is (re-find #"purpose=transaction" html)
|
||||
"Simple mode account typeahead URL must include purpose=transaction to enforce allowance rules"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC8: location dropdown — fixed location vs full client list
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest location-select-options-test
|
||||
(testing "AC8: when account has a fixed location, dropdown shows only that location"
|
||||
(let [html (hiccup/html
|
||||
(location-select* {:name "location"
|
||||
:account-location "HQ"
|
||||
:client-locations ["DT" "NY"]
|
||||
:value "HQ"}))]
|
||||
(is (re-find #"HQ" html)
|
||||
"Fixed account location 'HQ' should appear in dropdown")
|
||||
(is (not (re-find #"DT" html))
|
||||
"Client location 'DT' should NOT appear when account has fixed location")
|
||||
(is (not (re-find #"Shared" html))
|
||||
"'Shared' option should not appear when account has fixed location")))
|
||||
|
||||
(testing "AC8: when account has no fixed location, dropdown shows full client location list"
|
||||
(let [html (hiccup/html
|
||||
(location-select* {:name "location"
|
||||
:account-location nil
|
||||
:client-locations ["DT" "NY"]
|
||||
:value "Shared"}))]
|
||||
(is (re-find #"Shared" html)
|
||||
"'Shared' option should appear when no fixed account location")
|
||||
(is (re-find #"DT" html)
|
||||
"Client location 'DT' should appear when no fixed account location")
|
||||
(is (re-find #"NY" html)
|
||||
"Client location 'NY' should appear when no fixed account location"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC9: simple → advanced toggle with pre-populated row
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest toggle-simple-to-advanced-test
|
||||
(testing "AC9: clicking 'Switch to advanced mode' from simple mode re-renders in advanced with one row pre-populated"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "TOGGLECL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Toggle Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
simple-account [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts simple-account}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/accounts simple-account})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-wizard-toggle-mode-handler request)
|
||||
body (:body response)]
|
||||
(is (re-find #"manual-coding-section" body)
|
||||
"Response body should contain the coding section element")
|
||||
;; Advanced mode has Switch to simple mode link (when <=1 row)
|
||||
(is (re-find #"Switch to simple mode" body)
|
||||
"After toggling to advanced mode (from simple with 1 row), 'Switch to simple mode' should appear")
|
||||
;; The account-grid-body section should be present in the advanced mode HTML
|
||||
(is (re-find #"account-grid-body" body)
|
||||
"Advanced mode response should contain the account-grid-body element")
|
||||
;; The account ID from the simple-mode row should appear in the advanced table HTML
|
||||
(is (re-find (re-pattern (str account-id)) body)
|
||||
"Advanced mode response should contain the account ID from the simple-mode row")
|
||||
;; The account name should appear in the advanced table HTML
|
||||
(is (re-find #"Toggle Account" body)
|
||||
"Advanced mode response should contain the account name from the simple-mode row"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC10: blank simple → advanced gives empty table
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest toggle-blank-simple-to-advanced-test
|
||||
(testing "AC10: 'Switch to advanced mode' from blank simple mode gives advanced with empty table"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "BLNKTOGLCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts []}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/accounts []})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-wizard-toggle-mode-handler request)
|
||||
body (:body response)]
|
||||
;; Should render in advanced mode
|
||||
(is (re-find #"account-grid-body" body)
|
||||
"Blank simple → advanced should produce account-grid-body")
|
||||
;; Should NOT have any account-row elements (empty grid)
|
||||
(is (not (re-find #"account-row" body))
|
||||
"Blank simple → advanced should have NO account rows")
|
||||
;; Should show 'Switch to simple mode' because 0 rows <= 1
|
||||
(is (re-find #"Switch to simple mode" body)
|
||||
"Advanced mode with 0 rows should show 'Switch to simple mode' link"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC11-12: "Switch to simple mode" visibility based on row count
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest advanced-mode-switch-link-visibility-test
|
||||
(testing "AC11: 'Switch to simple mode' link is visible in advanced mode with 0 or 1 rows"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "VISLINKCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Visibility Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
zero-rows []
|
||||
one-row [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
make-req (fn [accounts]
|
||||
{:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts accounts}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/accounts accounts})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}})
|
||||
html-zero-rows (hiccup/html
|
||||
(fc/start-form (:multi-form-state (make-req zero-rows)) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :advanced (make-req zero-rows)))))
|
||||
html-one-row (hiccup/html
|
||||
(fc/start-form (:multi-form-state (make-req one-row)) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :advanced (make-req one-row)))))]
|
||||
(is (re-find #"Switch to simple mode" html-zero-rows)
|
||||
"In advanced mode with 0 rows, 'Switch to simple mode' should be visible")
|
||||
(is (re-find #"Switch to simple mode" html-one-row)
|
||||
"In advanced mode with 1 row, 'Switch to simple mode' should be visible")))
|
||||
|
||||
(testing "AC12: 'Switch to simple mode' link is absent in advanced mode with 2+ rows"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "TWOROWCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Account1"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "account-id-2"
|
||||
:account/name "Account2"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
account-id-2 (tempid->id result "account-id-2")
|
||||
client-id (tempid->id result "client-id")
|
||||
two-rows [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 50.0}
|
||||
{:db/id "row-2"
|
||||
:transaction-account/account account-id-2
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 50.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts two-rows}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/accounts two-rows})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
html (hiccup/html
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :advanced request))))]
|
||||
(is (not (re-find #"Switch to simple mode" html))
|
||||
"In advanced mode with 2+ rows, 'Switch to simple mode' link should be absent"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC13: advanced → simple (1 row) re-renders in simple mode
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest toggle-advanced-to-simple-test
|
||||
(testing "AC13: 'Switch to simple mode' from advanced (1 row) gives simple mode with that row's values"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "TOGSIMCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Toggle Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
one-row [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts one-row}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/accounts one-row})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-wizard-toggle-mode-handler request)
|
||||
body (:body response)]
|
||||
;; After toggling from advanced to simple, show "Switch to advanced mode" link
|
||||
(is (re-find #"Switch to advanced mode" body)
|
||||
"After toggling to simple mode, 'Switch to advanced mode' should be visible")
|
||||
;; The row's account ID should appear in the simple mode form
|
||||
(is (re-find (re-pattern (str account-id)) body)
|
||||
"Simple mode should display the account from the advanced mode row"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC14: vendor change in advanced mode with no rows creates one row
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest edit-vendor-changed-advanced-mode-no-rows-test
|
||||
(testing "AC14: vendor change in advanced mode with no rows creates one row with vendor's default account"
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "Advanced Vendor"}
|
||||
{:db/id "account-id"
|
||||
:account/name "Vendor Default Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/default-account "account-id"}
|
||||
{:db/id "client-id"
|
||||
:client/code "ADVVCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 200.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
;; Advanced mode with no accounts
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts []}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/accounts []})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 200.0}}
|
||||
response (edit-vendor-changed-handler request)
|
||||
body (:body response)]
|
||||
(is (re-find #"manual-coding-section" body))
|
||||
;; Should have account-grid-body (advanced mode)
|
||||
(is (re-find #"account-grid-body" body)
|
||||
"Advanced mode vendor change should show account-grid-body")
|
||||
;; The vendor's default account ID should appear in the new row
|
||||
(is (re-find (re-pattern (str account-id)) body)
|
||||
"Advanced mode: vendor default account should appear in the newly created row")
|
||||
(is (re-find #"Vendor Default Account" body)
|
||||
"Advanced mode: vendor default account name should appear in the new row"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC15: vendor change in advanced mode with existing rows does NOT modify them
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest edit-vendor-changed-advanced-mode-existing-rows-test
|
||||
(testing "AC15: vendor change in advanced mode with existing rows does NOT modify existing rows"
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "New Vendor"}
|
||||
{:db/id "vendor-account-id"
|
||||
:account/name "Vendor Default Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/default-account "vendor-account-id"}
|
||||
{:db/id "existing-account-id"
|
||||
:account/name "Existing Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-id"
|
||||
:client/code "ADVEXCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
vendor-account-id (tempid->id result "vendor-account-id")
|
||||
existing-account-id (tempid->id result "existing-account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
existing-rows [{:db/id "row-1"
|
||||
:transaction-account/account existing-account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
;; Advanced mode with existing accounts — vendor change should NOT touch them
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts existing-rows}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/accounts existing-rows})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
response (edit-vendor-changed-handler request)
|
||||
body (:body response)]
|
||||
;; The original (existing) account should still be shown
|
||||
(is (re-find (re-pattern (str existing-account-id)) body)
|
||||
"Existing row account should remain unchanged when vendor changes in advanced mode")
|
||||
;; The vendor's default account should NOT appear — existing rows are untouched
|
||||
(is (not (re-find (re-pattern (str vendor-account-id)) body))
|
||||
"Vendor default account should NOT replace existing rows in advanced mode"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC16: vendor-default-account uses clientized vendor logic
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest vendor-default-account-clientize-test
|
||||
(testing "AC16: client-specific account override takes precedence over global vendor default"
|
||||
(let [result @(dc/transact conn [{:db/id "global-account-id"
|
||||
:account/name "Global Default Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-specific-account-id"
|
||||
:account/name "Client Specific Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-id"
|
||||
:client/code "CLACCTEST"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/name "Clientized Vendor"
|
||||
:vendor/default-account "global-account-id"
|
||||
:vendor/account-overrides [{:vendor-account-override/client "client-id"
|
||||
:vendor-account-override/account "client-specific-account-id"}]}])
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
client-specific-account-id (tempid->id result "client-specific-account-id")
|
||||
result-account (vendor-default-account vendor-id client-id)]
|
||||
(is (some? result-account)
|
||||
"Should return a default account")
|
||||
(is (= client-specific-account-id (:db/id result-account))
|
||||
"Client-specific account override should take precedence over global vendor default")))
|
||||
|
||||
(testing "AC16: falls back to global vendor default when no client override"
|
||||
(let [result @(dc/transact conn [{:db/id "global-account-id"
|
||||
:account/name "Global Default Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "vendor-id"
|
||||
:vendor/name "Vendor No Override"
|
||||
:vendor/default-account "global-account-id"}
|
||||
{:db/id "client-id"
|
||||
:client/code "NOOVCL"
|
||||
:client/locations ["DT"]}])
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
global-account-id (tempid->id result "global-account-id")
|
||||
result-account (vendor-default-account vendor-id client-id)]
|
||||
(is (= global-account-id (:db/id result-account))
|
||||
"Should use global vendor default when no client-specific override exists")))
|
||||
|
||||
(testing "AC16: clientize-vendor applies both terms and account overrides"
|
||||
(let [vendor {:db/id "vendor-id"
|
||||
:vendor/name "Test Vendor"
|
||||
:vendor/default-account {:db/id "global-account-id"}
|
||||
:vendor/terms "NET30"
|
||||
:vendor/automatically-paid-when-due []
|
||||
:vendor/terms-overrides [{:vendor-terms-override/client {:db/id "client-id"}
|
||||
:vendor-terms-override/terms "NET60"}]
|
||||
:vendor/account-overrides [{:vendor-account-override/client {:db/id "client-id"}
|
||||
:vendor-account-override/account {:db/id "client-specific-account-id"}}]}
|
||||
clientized (clientize-vendor vendor "client-id")]
|
||||
(is (= "NET60" (:vendor/terms clientized))
|
||||
"Terms override should be applied")
|
||||
(is (= "client-specific-account-id" (-> clientized :vendor/default-account :db/id))
|
||||
"Account override should be applied"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC17: approval status, memo, vendor fields present in both modes
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest fields-present-in-both-modes-test
|
||||
;; NOTE: memo and approval-status fields are rendered by LinksStep/render-step
|
||||
;; (outside of #manual-coding-section), so they cannot be tested at the
|
||||
;; manual-coding-section* level. This test covers what manual-coding-section*
|
||||
;; is directly responsible for.
|
||||
(testing "AC17: key fields are present in both simple and advanced modes"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "FLDTESTCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Fields Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
one-account [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
make-req (fn [mode accounts]
|
||||
{:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts accounts}
|
||||
[]
|
||||
{:mode (name mode)
|
||||
:transaction/accounts accounts})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}})
|
||||
html-simple (hiccup/html
|
||||
(fc/start-form (:multi-form-state (make-req :simple one-account)) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :simple (make-req :simple one-account)))))
|
||||
html-advanced (hiccup/html
|
||||
(fc/start-form (:multi-form-state (make-req :advanced one-account)) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :advanced (make-req :advanced one-account)))))]
|
||||
;; --- Vendor field assertions (both modes) ---
|
||||
(is (re-find #"Vendor" html-simple)
|
||||
"AC17: Vendor field label must appear in simple mode")
|
||||
(is (re-find #"vendor/search" html-simple)
|
||||
"AC17: Vendor typeahead/search URL must appear in simple mode")
|
||||
(is (re-find #"transaction/vendor" html-simple)
|
||||
"AC17: Vendor field name must appear in simple mode")
|
||||
(is (re-find #"Vendor" html-advanced)
|
||||
"AC17: Vendor field label must appear in advanced mode")
|
||||
(is (re-find #"vendor/search" html-advanced)
|
||||
"AC17: Vendor typeahead/search URL must appear in advanced mode")
|
||||
(is (re-find #"transaction/vendor" html-advanced)
|
||||
"AC17: Vendor field name must appear in advanced mode")
|
||||
;; --- Hidden mode input (proves section is complete) ---
|
||||
(is (re-find #"name=\"mode\".*value=\"simple\"" html-simple)
|
||||
"AC17: hidden mode=simple input must be present in simple mode")
|
||||
(is (re-find #"name=\"mode\".*value=\"advanced\"" html-advanced)
|
||||
"AC17: hidden mode=advanced input must be present in advanced mode")
|
||||
;; --- Simple-mode-specific fields: account typeahead, location select, toggle link ---
|
||||
(is (re-find #"account/search" html-simple)
|
||||
"AC17: account typeahead search URL must appear in simple mode")
|
||||
(is (re-find #"transaction-account/location" html-simple)
|
||||
"AC17: location select field name must appear in simple mode")
|
||||
(is (re-find #"Switch to advanced mode" html-simple)
|
||||
"AC17: toggle link to advanced mode must appear in simple mode")
|
||||
;; --- Advanced-mode-specific: account-grid-body and toggle link ---
|
||||
(is (re-find #"account-grid-body" html-advanced)
|
||||
"AC17: #account-grid-body must be present in advanced mode")
|
||||
(is (re-find #"Switch to simple mode" html-advanced)
|
||||
"AC17: toggle link to simple mode must appear in advanced mode (<=1 row)"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC18: switching modes mid-edit then saving produces a valid transaction
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest switch-modes-then-save-test
|
||||
(testing "AC18: switching simple→advanced→save produces a single valid transaction row in DB"
|
||||
(let [result @(dc/transact conn [{:db/id "vendor-id"
|
||||
:vendor/name "Switch Vendor"}
|
||||
{:db/id "account-id"
|
||||
:account/name "Switch Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "client-id"
|
||||
:client/code "SWITCHCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
vendor-id (tempid->id result "vendor-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
simple-row [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
;; Step 1: Start in simple mode
|
||||
simple-request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts simple-row}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/accounts simple-row})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
;; Step 2: Toggle to advanced mode
|
||||
toggle-response (edit-wizard-toggle-mode-handler simple-request)
|
||||
;; After toggle, build a save request in advanced mode with one row
|
||||
advanced-snapshot {:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:action :manual
|
||||
:transaction/vendor vendor-id
|
||||
:transaction/amount 100.0
|
||||
:transaction/accounts [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]}
|
||||
save-request {:multi-form-state (mm/->MultiStepFormState advanced-snapshot [] advanced-snapshot)
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}
|
||||
:identity {:user/role "admin"}}]
|
||||
;; Verify the toggle worked
|
||||
(is (re-find #"account-grid-body" (:body toggle-response))
|
||||
"After toggle, response should be in advanced mode")
|
||||
;; Now save
|
||||
(with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))]
|
||||
(save-handler save-request))
|
||||
;; Verify exactly one account row was saved (no orphaned rows)
|
||||
(let [saved (dc/pull (dc/db conn)
|
||||
'[:db/id
|
||||
{:transaction/accounts [{:transaction-account/account [:db/id]}
|
||||
:transaction-account/location
|
||||
:transaction-account/amount]}]
|
||||
tx-id)]
|
||||
(is (= 1 (count (:transaction/accounts saved)))
|
||||
"AC18: Exactly one account row should be saved after mode-switch + save (no orphaned rows)")
|
||||
(is (= account-id (-> saved :transaction/accounts first :transaction-account/account :db/id))
|
||||
"AC18: The correct account should be saved")))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC19: split transaction re-opens in advanced mode with all splits
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest split-transaction-reopens-advanced-test
|
||||
(testing "AC19: split transaction (2+ accounts) re-opens in advanced mode with all splits intact"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "SPLITCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id-1"
|
||||
:account/name "Split Account 1"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "account-id-2"
|
||||
:account/name "Split Account 2"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id-1 (tempid->id result "account-id-1")
|
||||
account-id-2 (tempid->id result "account-id-2")
|
||||
client-id (tempid->id result "client-id")
|
||||
two-rows [{:db/id "row-1"
|
||||
:transaction-account/account account-id-1
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 50.0}
|
||||
{:db/id "row-2"
|
||||
:transaction-account/account account-id-2
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 50.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts two-rows}
|
||||
[]
|
||||
{:mode "advanced"
|
||||
:transaction/accounts two-rows})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
html (hiccup/html
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :advanced request))))]
|
||||
;; Should show both account IDs
|
||||
(is (re-find (re-pattern (str account-id-1)) html)
|
||||
"AC19: First split account should appear in advanced mode rendering")
|
||||
(is (re-find (re-pattern (str account-id-2)) html)
|
||||
"AC19: Second split account should appear in advanced mode rendering")
|
||||
;; Should NOT show 'Switch to simple mode' with 2 rows
|
||||
(is (not (re-find #"Switch to simple mode" html))
|
||||
"AC19: Split transaction (2 rows) in advanced mode should NOT show 'Switch to simple mode'"))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; AC20: single-account transaction re-opens in simple mode
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest single-account-reopens-simple-test
|
||||
(testing "AC20: single-account transaction re-opens in simple mode with account/location pre-populated"
|
||||
(let [result @(dc/transact conn [{:db/id "client-id"
|
||||
:client/code "SINGCL"
|
||||
:client/locations ["DT"]}
|
||||
{:db/id "account-id"
|
||||
:account/name "Single Account"
|
||||
:account/type :account-type/expense}
|
||||
{:db/id "transaction-id"
|
||||
:transaction/amount 100.0
|
||||
:transaction/date #inst "2023-01-01"
|
||||
:transaction/id (str (java.util.UUID/randomUUID))
|
||||
:transaction/client "client-id"}])
|
||||
tx-id (tempid->id result "transaction-id")
|
||||
account-id (tempid->id result "account-id")
|
||||
client-id (tempid->id result "client-id")
|
||||
one-account [{:db/id "row-1"
|
||||
:transaction-account/account account-id
|
||||
:transaction-account/location "DT"
|
||||
:transaction-account/amount 100.0}]
|
||||
request {:multi-form-state (mm/->MultiStepFormState
|
||||
{:db/id tx-id
|
||||
:transaction/client client-id
|
||||
:transaction/accounts one-account}
|
||||
[]
|
||||
{:mode "simple"
|
||||
:transaction/accounts one-account})
|
||||
:entity {:db/id tx-id
|
||||
:transaction/client {:db/id client-id}
|
||||
:transaction/amount 100.0}}
|
||||
html (hiccup/html
|
||||
(fc/start-form (:multi-form-state request) nil
|
||||
(fc/with-field :step-params
|
||||
(manual-coding-section* :simple request))))]
|
||||
;; Simple mode should show the account typeahead pre-populated with the account
|
||||
(is (re-find (re-pattern (str account-id)) html)
|
||||
"AC20: Simple mode should show the account pre-populated")
|
||||
;; Should show 'Switch to advanced mode' (confirming it's in simple mode)
|
||||
(is (re-find #"Switch to advanced mode" html)
|
||||
"AC20: Simple mode should show 'Switch to advanced mode' link")
|
||||
;; Should NOT show 'Switch to simple mode'
|
||||
(is (not (re-find #"Switch to simple mode" html))
|
||||
"AC20: Simple mode should NOT show 'Switch to simple mode' link"))))
|
||||
Reference in New Issue
Block a user