@@ -6,6 +6,9 @@ package app
66import (
77 "errors"
88 "fmt"
9+ "net/http"
10+ "os"
11+ "path/filepath"
912 "strconv"
1013 "strings"
1114 "time"
@@ -102,6 +105,13 @@ type vendorFormData struct {
102105 Notes string
103106}
104107
108+ type documentFormData struct {
109+ Title string
110+ FilePath string // local file path; read on submit for new documents
111+ EntityKind string // set by scoped handlers
112+ Notes string
113+ }
114+
105115type applianceFormData struct {
106116 Name string
107117 Brand string
@@ -114,12 +124,6 @@ type applianceFormData struct {
114124 Notes string
115125}
116126
117- type documentFormData struct {
118- Title string
119- SourcePath string // filesystem path to import from (empty on metadata-only edits)
120- Notes string
121- }
122-
123127func (m * Model ) startHouseForm () {
124128 values := & houseFormData {}
125129 if m .hasHouse {
@@ -644,78 +648,6 @@ func (m *Model) parseVendorFormData() (data.Vendor, error) {
644648 }, nil
645649}
646650
647- func (m * Model ) startDocumentForm () {
648- values := & documentFormData {}
649- m .openDocumentForm (values )
650- }
651-
652- func (m * Model ) startEditDocumentForm (id uint ) error {
653- doc , err := m .store .GetDocument (id )
654- if err != nil {
655- return fmt .Errorf ("load document: %w" , err )
656- }
657- values := documentFormValues (doc )
658- m .editID = & id
659- m .openDocumentForm (values )
660- return nil
661- }
662-
663- func (m * Model ) openDocumentForm (values * documentFormData ) {
664- filePickerValidation := requiredText ("document file" )
665- if m .editID != nil {
666- // Editing: file picker is optional (keep existing content if blank).
667- filePickerValidation = nil
668- }
669- form := huh .NewForm (
670- huh .NewGroup (
671- huh .NewInput ().
672- Title ("Title" ).
673- Placeholder ("auto-filled from filename if blank" ).
674- Value (& values .Title ),
675- huh .NewFilePicker ().
676- Title ("Document file" ).
677- Description ("Pick a file to import (leave blank to keep current)" ).
678- FileAllowed (true ).
679- DirAllowed (false ).
680- ShowHidden (false ).
681- Value (& values .SourcePath ).
682- Validate (filePickerValidation ),
683- huh .NewText ().Title ("Notes" ).Value (& values .Notes ),
684- ).Title ("File" ),
685- )
686- m .activateForm (formDocument , form , values )
687- }
688-
689- func (m * Model ) submitDocumentForm () error {
690- doc , sourcePath , err := m .parseDocumentFormData ()
691- if err != nil {
692- return err
693- }
694- if m .editID != nil {
695- doc .ID = * m .editID
696- return m .store .UpdateDocument (doc , sourcePath )
697- }
698- return m .store .CreateDocument (doc , sourcePath )
699- }
700-
701- func (m * Model ) parseDocumentFormData () (data.Document , string , error ) {
702- values , ok := m .formData .(* documentFormData )
703- if ! ok {
704- return data.Document {}, "" , fmt .Errorf ("unexpected document form data" )
705- }
706- return data.Document {
707- Title : strings .TrimSpace (values .Title ),
708- Notes : strings .TrimSpace (values .Notes ),
709- }, strings .TrimSpace (values .SourcePath ), nil
710- }
711-
712- func documentFormValues (doc data.Document ) * documentFormData {
713- return & documentFormData {
714- Title : doc .Title ,
715- Notes : doc .Notes ,
716- }
717- }
718-
719651func (m * Model ) inlineEditVendor (id uint , col int ) error {
720652 vendor , err := m .store .GetVendor (id )
721653 if err != nil {
@@ -1732,3 +1664,162 @@ func intToString(value int) string {
17321664 }
17331665 return strconv .Itoa (value )
17341666}
1667+
1668+ // ---------------------------------------------------------------------------
1669+ // Document forms
1670+ // ---------------------------------------------------------------------------
1671+
1672+ // startDocumentForm opens a new-document form. entityKind is set by scoped
1673+ // handlers (e.g. "project") or empty for the top-level Documents tab.
1674+ func (m * Model ) startDocumentForm (entityKind string ) error {
1675+ values := & documentFormData {EntityKind : entityKind }
1676+ form := huh .NewForm (
1677+ huh .NewGroup (
1678+ huh .NewInput ().
1679+ Title ("Title" ).
1680+ Value (& values .Title ).
1681+ Validate (requiredText ("title" )),
1682+ huh .NewInput ().
1683+ Title ("File path" ).
1684+ Description ("Local path to the file to attach" ).
1685+ Value (& values .FilePath ).
1686+ Validate (optionalFilePath ()),
1687+ huh .NewText ().Title ("Notes" ).Value (& values .Notes ),
1688+ ),
1689+ )
1690+ m .activateForm (formDocument , form , values )
1691+ return nil
1692+ }
1693+
1694+ func (m * Model ) startEditDocumentForm (id uint ) error {
1695+ doc , err := m .store .GetDocument (id )
1696+ if err != nil {
1697+ return fmt .Errorf ("load document: %w" , err )
1698+ }
1699+ values := documentFormValues (doc )
1700+ m .editID = & id
1701+ m .openEditDocumentForm (values )
1702+ return nil
1703+ }
1704+
1705+ func (m * Model ) openEditDocumentForm (values * documentFormData ) {
1706+ form := huh .NewForm (
1707+ huh .NewGroup (
1708+ huh .NewInput ().
1709+ Title ("Title" ).
1710+ Value (& values .Title ).
1711+ Validate (requiredText ("title" )),
1712+ huh .NewText ().Title ("Notes" ).Value (& values .Notes ),
1713+ ),
1714+ )
1715+ m .activateForm (formDocument , form , values )
1716+ }
1717+
1718+ func (m * Model ) submitDocumentForm () error {
1719+ doc , err := m .parseDocumentFormData ()
1720+ if err != nil {
1721+ return err
1722+ }
1723+ if m .editID != nil {
1724+ doc .ID = * m .editID
1725+ return m .store .UpdateDocument (doc )
1726+ }
1727+ return m .store .CreateDocument (doc )
1728+ }
1729+
1730+ func (m * Model ) parseDocumentFormData () (data.Document , error ) {
1731+ values , ok := m .formData .(* documentFormData )
1732+ if ! ok {
1733+ return data.Document {}, fmt .Errorf ("unexpected document form data" )
1734+ }
1735+ doc := data.Document {
1736+ Title : strings .TrimSpace (values .Title ),
1737+ EntityKind : values .EntityKind ,
1738+ Notes : strings .TrimSpace (values .Notes ),
1739+ }
1740+ // Read file from path if provided (new document creation).
1741+ path := filepath .Clean (strings .TrimSpace (values .FilePath ))
1742+ if path != "" && path != "." {
1743+ fileData , err := os .ReadFile (path )
1744+ if err != nil {
1745+ return data.Document {}, fmt .Errorf ("read file: %w" , err )
1746+ }
1747+ doc .Data = fileData
1748+ doc .SizeBytes = int64 (len (fileData ))
1749+ doc .MIMEType = detectMIMEType (path , fileData )
1750+ }
1751+ return doc , nil
1752+ }
1753+
1754+ func (m * Model ) inlineEditDocument (id uint , col int ) error {
1755+ doc , err := m .store .GetDocument (id )
1756+ if err != nil {
1757+ return fmt .Errorf ("load document: %w" , err )
1758+ }
1759+ values := documentFormValues (doc )
1760+ // Column mapping: 0=ID, 1=Title, 2=Entity(ro), 3=Type(ro), 4=Size(ro), 5=Notes, 6=Updated(ro)
1761+ switch col {
1762+ case 1 :
1763+ m .openInlineInput (
1764+ id ,
1765+ formDocument ,
1766+ "Title" ,
1767+ "" ,
1768+ & values .Title ,
1769+ requiredText ("title" ),
1770+ values ,
1771+ )
1772+ case 5 :
1773+ m .openInlineInput (id , formDocument , "Notes" , "" , & values .Notes , nil , values )
1774+ default :
1775+ return m .startEditDocumentForm (id )
1776+ }
1777+ return nil
1778+ }
1779+
1780+ func documentFormValues (doc data.Document ) * documentFormData {
1781+ return & documentFormData {
1782+ Title : doc .Title ,
1783+ EntityKind : doc .EntityKind ,
1784+ Notes : doc .Notes ,
1785+ }
1786+ }
1787+
1788+ // detectMIMEType uses http.DetectContentType with a file extension fallback.
1789+ func detectMIMEType (path string , fileData []byte ) string {
1790+ mime := http .DetectContentType (fileData )
1791+ // DetectContentType returns application/octet-stream for unknown types;
1792+ // try extension-based detection as a fallback.
1793+ if mime == "application/octet-stream" {
1794+ switch strings .ToLower (filepath .Ext (path )) {
1795+ case ".pdf" :
1796+ return "application/pdf"
1797+ case ".txt" :
1798+ return "text/plain"
1799+ case ".csv" :
1800+ return "text/csv"
1801+ case ".json" :
1802+ return "application/json"
1803+ case ".md" :
1804+ return "text/markdown"
1805+ }
1806+ }
1807+ return mime
1808+ }
1809+
1810+ func optionalFilePath () func (string ) error {
1811+ return func (input string ) error {
1812+ path := strings .TrimSpace (input )
1813+ if path == "" {
1814+ return nil
1815+ }
1816+ info , err := os .Stat (path )
1817+ if err != nil {
1818+ return fmt .Errorf ("file not found: %s" , path )
1819+ }
1820+ if info .IsDir () {
1821+ return fmt .Errorf ("path is a directory, not a file" )
1822+ }
1823+ return nil
1824+ }
1825+ }
0 commit comments