234 lines
7.1 KiB
Go
234 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const uploadsDir = "data/uploads"
|
|
|
|
func (h *Handler) handleFileUpload(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
profile := getProfile(r.Context())
|
|
|
|
err := r.ParseMultipartForm(32 << 20) // 32MB max
|
|
if err != nil {
|
|
http.Error(w, "Error parsing form", 400)
|
|
return
|
|
}
|
|
|
|
dealID := r.FormValue("deal_id")
|
|
folderID := r.FormValue("folder_id")
|
|
requestItemID := r.FormValue("request_item_id")
|
|
|
|
if dealID == "" {
|
|
http.Error(w, "Deal ID is required", 400)
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
http.Error(w, "File is required", 400)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Create upload directory
|
|
dealDir := filepath.Join(uploadsDir, dealID)
|
|
os.MkdirAll(dealDir, 0755)
|
|
|
|
// Generate unique filename
|
|
fileID := generateID("file")
|
|
ext := filepath.Ext(header.Filename)
|
|
storageName := fileID + ext
|
|
storagePath := filepath.Join(dealDir, storageName)
|
|
|
|
// Save file to disk
|
|
dst, err := os.Create(storagePath)
|
|
if err != nil {
|
|
http.Error(w, "Error saving file", 500)
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
io.Copy(dst, file)
|
|
|
|
// Detect MIME type
|
|
mimeType := header.Header.Get("Content-Type")
|
|
if mimeType == "" {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
|
|
// Insert file record
|
|
_, err = h.db.Exec(`INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, storage_path, uploaded_by) VALUES (?, ?, ?, ?, ?, ?, 'uploaded', ?, ?)`,
|
|
fileID, dealID, folderID, header.Filename, header.Size, mimeType, storagePath, profile.ID)
|
|
if err != nil {
|
|
os.Remove(storagePath)
|
|
http.Error(w, "Error saving file record", 500)
|
|
return
|
|
}
|
|
|
|
// Link to request item if specified
|
|
if requestItemID != "" {
|
|
var existing string
|
|
h.db.QueryRow("SELECT linked_file_ids FROM diligence_requests WHERE id = ?", requestItemID).Scan(&existing)
|
|
if existing == "" {
|
|
existing = fileID
|
|
} else {
|
|
existing = existing + "," + fileID
|
|
}
|
|
h.db.Exec("UPDATE diligence_requests SET linked_file_ids = ? WHERE id = ?", existing, requestItemID)
|
|
}
|
|
|
|
// Create a response record for this document and enqueue extraction
|
|
respID := generateID("resp")
|
|
h.db.Exec(
|
|
`INSERT INTO responses (id, deal_id, type, title, file_id, extraction_status, created_by) VALUES (?, ?, 'document', ?, ?, 'pending', ?)`,
|
|
respID, dealID, header.Filename, fileID, profile.ID)
|
|
h.db.Exec("UPDATE files SET response_id = ? WHERE id = ?", respID, fileID)
|
|
h.enqueueExtraction(respID, storagePath, dealID)
|
|
|
|
// Log activity
|
|
h.logActivity(dealID, profile.ID, profile.OrganizationID, "upload", "file", header.Filename, fileID)
|
|
|
|
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleFileDelete(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
profile := getProfile(r.Context())
|
|
if profile.Role != "owner" && profile.Role != "admin" {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
fileID := r.FormValue("file_id")
|
|
dealID := r.FormValue("deal_id")
|
|
|
|
if fileID == "" {
|
|
http.Error(w, "Missing file ID", 400)
|
|
return
|
|
}
|
|
|
|
// Get storage path and delete from disk
|
|
var storagePath string
|
|
h.db.QueryRow("SELECT COALESCE(storage_path, '') FROM files WHERE id = ?", fileID).Scan(&storagePath)
|
|
if storagePath != "" {
|
|
os.Remove(storagePath)
|
|
}
|
|
|
|
h.db.Exec("DELETE FROM files WHERE id = ? AND deal_id = ?", fileID, dealID)
|
|
|
|
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleFileDownload(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
// Parse: /deals/files/download/{fileID}
|
|
fileID := strings.TrimPrefix(r.URL.Path, "/deals/files/download/")
|
|
if fileID == "" {
|
|
http.Error(w, "Missing file ID", 400)
|
|
return
|
|
}
|
|
|
|
var name, storagePath, dealID string
|
|
err := h.db.QueryRow("SELECT name, COALESCE(storage_path, ''), deal_id FROM files WHERE id = ?", fileID).Scan(&name, &storagePath, &dealID)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if storagePath == "" || !fileExists(storagePath) {
|
|
http.Error(w, "File not found on disk", 404)
|
|
return
|
|
}
|
|
|
|
// Log download activity
|
|
h.logActivity(dealID, profile.ID, profile.OrganizationID, "download", "file", name, fileID)
|
|
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
|
|
http.ServeFile(w, r, storagePath)
|
|
}
|
|
|
|
func (h *Handler) logActivity(dealID, userID, orgID, actType, resourceType, resourceName, resourceID string) {
|
|
id := generateID("act")
|
|
h.db.Exec(`INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, resource_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
id, orgID, dealID, userID, actType, resourceType, resourceName, resourceID)
|
|
}
|
|
|
|
func (h *Handler) handleFileComments(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
// Parse: /deals/files/comments/{fileID}
|
|
fileID := strings.TrimPrefix(r.URL.Path, "/deals/files/comments/")
|
|
|
|
// Get deal_id for this file
|
|
var dealID string
|
|
h.db.QueryRow("SELECT deal_id FROM files WHERE id = ?", fileID).Scan(&dealID)
|
|
|
|
if r.Method == http.MethodPost {
|
|
content := strings.TrimSpace(r.FormValue("content"))
|
|
if content != "" {
|
|
id := generateID("cmt")
|
|
h.db.Exec("INSERT INTO file_comments (id, file_id, deal_id, user_id, content) VALUES (?, ?, ?, ?, ?)",
|
|
id, fileID, dealID, profile.ID, content)
|
|
}
|
|
}
|
|
|
|
// Get comments
|
|
rows, err := h.db.Query(`
|
|
SELECT c.id, c.content, c.created_at, COALESCE(p.full_name, 'Unknown')
|
|
FROM file_comments c LEFT JOIN profiles p ON c.user_id = p.id
|
|
WHERE c.file_id = ? ORDER BY c.created_at ASC`, fileID)
|
|
if err != nil {
|
|
http.Error(w, "Error loading comments", 500)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
// Build HTML response
|
|
html := `<div class="space-y-3 max-h-64 overflow-y-auto mb-4">`
|
|
hasComments := false
|
|
for rows.Next() {
|
|
hasComments = true
|
|
var id, content, userName string
|
|
var createdAt string
|
|
rows.Scan(&id, &content, &createdAt, &userName)
|
|
html += fmt.Sprintf(`<div class="bg-gray-800 rounded-lg p-3">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="text-xs font-medium text-teal-400">%s</span>
|
|
<span class="text-xs text-gray-600">%s</span>
|
|
</div>
|
|
<p class="text-sm text-gray-300">%s</p>
|
|
</div>`, userName, createdAt, content)
|
|
}
|
|
if !hasComments {
|
|
html += `<p class="text-sm text-gray-500 italic">No comments yet.</p>`
|
|
}
|
|
html += `</div>`
|
|
|
|
// Add comment form
|
|
html += fmt.Sprintf(`<form hx-post="/deals/files/comments/%s" hx-target="#comments-%s" hx-swap="innerHTML" class="flex gap-2">
|
|
<input type="text" name="content" placeholder="Add a comment..." class="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
<button type="submit" class="px-3 py-2 bg-teal-500 text-white text-sm rounded-lg hover:bg-teal-600 transition">Send</button>
|
|
</form>`, fileID, fileID)
|
|
|
|
w.Write([]byte(html))
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|