diff --git a/README.md b/README.md index 9d9bf92d96..ad59141e17 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# wacrm — CRM Template for WhatsApp +# waivy — CRM inteligente para WhatsApp -> Self-hostable CRM template for WhatsApp® — shared inbox, contacts, -> sales pipelines, broadcasts, and no-code automations. Fork it, brand +> waivy — CRM inteligente para WhatsApp® — bandeja compartida, contactos, +> pipeline de ventas, broadcasts, automaciones sin código. Fork it, brand > it, host it.

diff --git a/messages/en.json b/messages/en.json index a3dd25ff20..9b34d2e795 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1275,7 +1275,7 @@ "labelTooLong": "Label must be {max} characters or fewer", "copied": "Invite link copied", "clipboardBlocked": "Clipboard blocked — copy the link manually", - "whatsappMessage": "Join {accountName} on wacrm using this link (valid for {expiresInDays} days): {url}" + "whatsappMessage": "Join {accountName} on waivy using this link (valid for {expiresInDays} days): {url}" }, "tagsAndFields": { "title": "Fields & tags", @@ -1395,8 +1395,8 @@ "submitApproval": "Submit for Approval", "deleteDialogTitle": "Delete template?", - "deleteMetaDesc": "\"{name}\" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.", - "deleteLocalDesc": "\"{name}\" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.", + "deleteMetaDesc": "\"{name}\" will be deleted from Meta and from waivy. Active broadcasts using this template will start failing on their next send. This can't be undone.", + "deleteLocalDesc": "\"{name}\" will be deleted from waivy. It was never submitted to Meta, so no remote cleanup is needed.", "deleting": "Deleting…", "delete": "Delete", @@ -1438,7 +1438,7 @@ "notConnected": "Not Connected", "connectedDesc": "Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.", "notConnectedDesc": "Configure your Meta API credentials below to connect your WhatsApp Business account.", - "registered": "Registered — Meta will deliver events to wacrm", + "registered": "Registered — Meta will deliver events to waivy", "notRegistered": "Not registered — Meta will not deliver events", "verifyWithMeta": "Verify with Meta", "subscribedSince": "Subscribed since {date}. Click Verify with Meta if events stop arriving.", @@ -1462,7 +1462,7 @@ "twoStepPin": "Two-step verification PIN", "optional": "(optional)", "pinPlaceholder": "6-digit PIN from Meta WhatsApp Manager", - "pinHint": "Needed only to wire inbound messages for a production number. Set it in Meta Business Manager → WhatsApp Accounts → Phone Numbers → Two-step verification, then paste it here so wacrm can subscribe the number — otherwise Meta routes inbound events to whichever app last claimed it (the symptom that hits second numbers under a shared WABA). Meta test numbers have no PIN and are pre-registered — leave this blank for them. Leaving it blank also keeps an existing registration untouched.", + "pinHint": "Needed only to wire inbound messages for a production number. Set it in Meta Business Manager → WhatsApp Accounts → Phone Numbers → Two-step verification, then paste it here so waivy can subscribe the number — otherwise Meta routes inbound events to whichever app last claimed it (the symptom that hits second numbers under a shared WABA). Meta test numbers have no PIN and are pre-registered — leave this blank for them. Leaving it blank also keeps an existing registration untouched.", "webhookTitle": "Webhook Configuration", "webhookDesc": "Use this URL as your webhook callback in the Meta App Dashboard.", "webhookUrl": "Webhook Callback URL", @@ -1629,7 +1629,7 @@ }, "aiConfig": { "title": "Agent setup", - "description": "Bring your own OpenAI or Anthropic key. wacrm calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.", + "description": "Bring your own OpenAI or Anthropic key waivy calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.", "adminOnlyConfig": "Only admins and owners can change the AI configuration.", "loadFailed": "Failed to load AI configuration", "providerAndKey": "Provider & key", diff --git a/messages/es.json b/messages/es.json new file mode 100644 index 0000000000..9b48a50e8c --- /dev/null +++ b/messages/es.json @@ -0,0 +1,1698 @@ +{ + "LoginPage": { + "titleAccept": "Sign in to accept", + "titleWelcome": "Welcome back", + "descAccept": "Sign in and we'll take you to the invitation.", + "descWelcome": "Sign in to your account", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "forgotPassword": "Forgot password?", + "passwordPlaceholder": "Enter your password", + "signingIn": "Signing in...", + "signIn": "Sign in", + "noAccount": "Don't have an account?", + "createAccount": "Create account" + }, + "Sidebar": { + "title": "waivy", + "dashboard": "Dashboard", + "inbox": "Inbox", + "notifications": "Notifications", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "flows": "Flows", + "aiAgents": "AI Agents", + "settings": "Settings", + "beta": "Beta", + "unreadConversations": "{count} unread {count, plural, =1 {conversation} other {conversations}}", + "unreadNotifications": "{count} unread {count, plural, =1 {notification} other {notifications}}", + "roleOwner": "Owner", + "roleAdmin": "Admin", + "roleAgent": "Agent", + "roleViewer": "Viewer", + "closeMenu": "Close menu", + "defaultUser": "User", + "defaultAvatar": "Avatar", + "menuProfile": "Profile", + "menuSettings": "Settings", + "menuSignOut": "Sign out" + }, + "Header": { + "dashboard": "Dashboard", + "inbox": "Inbox", + "notifications": "Notifications", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "settings": "Settings", + "openMenu": "Open menu", + "openAccountMenu": "Open account menu", + "defaultUser": "User", + "defaultAvatar": "Avatar", + "menuProfile": "Profile", + "menuSettings": "Settings", + "menuSignOut": "Sign out" + }, + "ModeToggle": { + "switchMode": "Switch to {mode} mode" + }, + "Dashboard": { + "page": { + "title": "Dashboard", + "description": "Live analytics across conversations, contacts, deals, broadcasts, and automations.", + "activeConversations": "Active Conversations", + "newContactsToday": "New Contacts Today", + "openDealsValue": "Open Deals Value", + "messagesSentToday": "Messages Sent Today", + "newTodayVsYesterday": "new today vs yesterday", + "vsYesterday": "vs yesterday", + "openDeals": "{count} open {count, plural, =1 {deal} other {deals}}", + "noChange": "No change {suffix}" + }, + "quickActions": { + "newContact": "New Contact", + "newDeal": "New Deal", + "newBroadcast": "New Broadcast", + "newAutomation": "New Automation" + }, + "activityFeed": { + "title": "Recent Activity", + "viewAll": "View all →", + "noActivity": "No activity yet", + "noActivityHint": "Activity from messages, deals, broadcasts, and automations will appear here.", + "showingOf": "Showing {visible} of {totalLoaded}{plus}", + "show": "Show", + "timeS": "{sec}s ago", + "timeM": "{min}m ago", + "timeH": "{hr}h ago", + "timeD": "{day}d ago" + }, + "conversationsChart": { + "title": "Conversations Over Time", + "description": "Daily message volume by direction", + "days": "{count} days", + "noActivity": "No message activity in this range", + "noActivityHint": "Send or receive messages to start populating this chart.", + "incoming": "Incoming", + "outgoing": "Outgoing", + "tooltipIncoming": "{count} incoming", + "tooltipOutgoing": "{count} outgoing", + "ariaLabel": "Conversations per day" + }, + "pipelineDonut": { + "title": "Pipeline Value", + "description": "Open deals by stage", + "noOpenDeals": "No open deals yet", + "noOpenDealsHint": "Create deals in Pipelines to see stage breakdowns here.", + "dealCount": "{count} {count, plural, =1 {deal} other {deals}}", + "total": "Total", + "ariaLabel": "Pipeline value by stage" + }, + "responseTimeChart": { + "title": "Average First Response Time", + "description": "Minutes to reply to a customer's first unreplied message, by weekday", + "target": "target {minutes}m", + "thisWeek": "This week:", + "lastWeek": "Last week:", + "noReplies": "No replies recorded yet", + "noRepliesHint": "This chart fills in as you reply to customer messages." + }, + "emptyState": { + "title": "Not enough data yet" + } + }, + "Inbox": { + "page": { + "whatsappNotConnected": "WhatsApp® is not connected. Go to Settings to connect your account." + }, + "conversationList": { + "searchPlaceholder": "Search conversations...", + "filterAll": "All", + "filterUnread": "Unread", + "filterOpen": "Open", + "filterPending": "Pending", + "filterClosed": "Closed", + "tags": "Tags", + "company": "Company", + "allCompanies": "All companies", + "clearAll": "Clear all", + "noConversations": "No conversations found", + "noMessagesYet": "No messages yet", + "unknown": "Unknown" + }, + "messageThread": { + "backToConversations": "Back to conversations", + "hideContactPanel": "Hide contact panel", + "showContactPanel": "Show contact panel", + "hideContact": "Hide contact", + "showContact": "Show contact", + "refresh": "Refresh", + "refreshConversation": "Refresh conversation", + "status": "Status", + "statusOpen": "Open", + "statusPending": "Pending", + "statusClosed": "Closed", + "assign": "Assign", + "assigned": "Assigned", + "unassign": "Unassign", + "noTeammates": "No teammates available", + "me": " (me)", + "noMessagesYet": "No messages yet", + "sendTemplateHint": "Send a template to start the conversation", + "selectConversation": "Select a conversation", + "selectConversationHint": "Choose a conversation from the left to start messaging", + "today": "Today", + "yesterday": "Yesterday" + }, + "sessionTimer": { + "expired": "Expired", + "xhRemaining": "{hours}h remaining", + "xmRemaining": "{minutes}m remaining", + "noCustomerMessages": "No customer messages" + }, + "composer": { + "draftHint": "Tap the ✨ to draft a reply with AI — you can edit it before sending", + "sessionExpiredHint": "24-hour session expired. Use a template to re-engage.", + "templates": "Templates", + "readOnlyPlaceholder": "Read-only — viewers can browse but not reply", + "sessionExpiredPlaceholder": "Session expired - use a template", + "typeMessagePlaceholder": "Type a message... (Shift+Enter for new line)", + "readOnlyTitle": "Read-only — your role can't send messages", + "recording": "Recording… {current} / {max}", + "cancel": "Cancel", + "stopAndAttach": "Stop and attach", + "attachMedia": "Attach media", + "photo": "Photo", + "video": "Video", + "document": "Document", + "voiceNote": "Voice note", + "sendTemplate": "Send template", + "draftWithAI": "Draft a reply with AI", + "addCaption": "Add a caption…", + "removeAttachment": "Remove attachment", + "moreActions": "More", + "interactiveMessage": "Interactive message", + "quickReplies": "Quick replies", + "quickRepliesEmpty": "No quick replies yet. Add some in Settings → Quick replies.", + "saveAsQuickReply": "Save as quick reply", + "send": "Send", + "quickReplyNamePrompt": "Name this quick reply:", + "quickReplySaved": "Saved as a quick reply.", + "quickReplySaveError": "Couldn't save the quick reply." + }, + "bubble": { + "photo": "Photo", + "video": "Video", + "audio": "Audio", + "unavailable": "{label} unavailable", + "document": "Document", + "locationShared": "Location shared", + "template": "Template", + "buttonReply": "Button reply", + "interactiveReply": "[Interactive reply]", + "unsupported": "[Unsupported message type]", + "aiBadge": "AI", + "aiBadgeTitle": "Sent automatically by the AI assistant" + }, + "actions": { + "reply": "Reply", + "copyText": "Copy text", + "delete": "Delete", + "react": "React", + "reactWith": "React with {emoji}", + "nothingToCopy": "Nothing to copy", + "copied": "Copied", + "copyFailed": "Copy failed" + }, + "replyQuote": { + "photo": "[Photo]", + "video": "[Video]", + "document": "[Document]", + "audio": "[Audio]", + "voiceMessage": "[Voice message]", + "location": "[Location]", + "template": "[Template]", + "message": "[Message]", + "cancelReply": "Cancel reply" + }, + "templatePicker": { + "sendTemplate": "Send template", + "fillPlaceholders": "Fill in the placeholders to render this template. Meta requires every variable to be set.", + "pickTemplate": "Pick an approved WhatsApp template to send to this contact.", + "noApprovedTemplates": "No approved templates", + "noApprovedTemplatesHint": "Approve a template in Meta WhatsApp Manager, then sync it from Settings → Templates.", + "preview": "Preview", + "headerValuePlaceholder": "Value for the header variable", + "bodyValuePlaceholder": "Value for {val}", + "urlSuffixValuePlaceholder": "URL suffix value", + "finalUrl": "Final URL: {url}", + "back": "Back", + "cancel": "Cancel", + "send": "Send template" + }, + "sidebar": { + "contactInfo": "Contact info", + "tags": "Tags", + "notes": "Notes", + "deals": "Deals", + "noTags": "No tags", + "noDeals": "No deals", + "addNotePlaceholder": "Add a note..." + }, + "aiBanner": { + "pausedTitle": "AI assistant is paused here", + "activeText": "AI assistant is replying automatically", + "takeOver": "Take over", + "resume": "Resume AI", + "tookOver": "You’ve taken over this chat.", + "resumed": "AI resumed.", + "updateError": "Couldn’t update the AI assistant.", + "networkError": "Couldn’t reach the server." + } + }, + "Contacts": { + "page": { + "title": "Contacts", + "subtitle": "Manage your contact list. {count} total contacts.", + "subtitleZero": "Manage your contact list.", + "customFieldsBtn": "Custom fields", + "importBtn": "Import", + "addContactBtn": "Add Contact", + "searchPlaceholder": "Search by name, phone, or email...", + "filterByTags": "Filter by tags", + "clearAll": "Clear all", + "noTagsYet": "No tags yet.", + "selectedCount": "{count} selected", + "clearSelection": "Clear", + "deleteSelected": "Delete selected", + "tableColumns": { + "name": "Name", + "phone": "Phone", + "email": "Email", + "company": "Company", + "tags": "Tags", + "createdAt": "Created" + }, + "loading": "Loading contacts...", + "noContactsMatch": "No contacts match your filters.", + "noContactsYet": "No contacts yet.", + "addFirstContact": "Add your first contact", + "unnamed": "Unnamed", + "editAction": "Edit", + "deleteAction": "Delete", + "showingPagination": "Showing {start}-{end} of {total}", + "pageCount": "Page {page} of {total}", + "deleteContactTitle": "Delete Contact", + "deleteContactDesc": "Are you sure you want to delete {name}? This action cannot be undone.", + "cancel": "Cancel", + "deleteBtn": "Delete", + "deleteBulkTitle": "Delete Contacts", + "deleteBulkDesc": "Are you sure you want to delete {count} selected contacts? This action cannot be undone.", + "toastFailedLoad": "Failed to load contacts", + "toastFailedDelete": "Failed to delete contact", + "toastDeleted": "Contact deleted", + "toastBulkFailedDelete": "Failed to delete contacts", + "toastBulkDeleted": "{count} contacts deleted" + }, + "form": { + "addTitle": "Add Contact", + "addDesc": "Create a new contact. Name is required.", + "editTitle": "Edit Contact", + "editDesc": "Update contact details and tags.", + "nameLabel": "Name", + "namePlaceholder": "Jane Doe", + "phoneLabel": "Phone Number", + "phonePlaceholder": "+1234567890", + "phoneHint": "Include country code, e.g. +1 for US", + "emailLabel": "Email", + "emailPlaceholder": "jane@example.com", + "companyLabel": "Company", + "companyPlaceholder": "Acme Corp", + "tagsLabel": "Tags", + "tagsPlaceholder": "Select tags...", + "noTagsFound": "No tags found.", + "createTagAction": "Create tag \"{value}\"", + "cancel": "Cancel", + "save": "Save Contact", + "update": "Update", + "create": "Create", + "toastSuccessAdd": "Contact created successfully", + "toastSuccessEdit": "Contact updated successfully", + "toastError": "Failed to save contact", + "toastConflict": "A contact with this phone number already exists", + "viewExisting": "View {name}", + "dupExact": "A contact with this phone number already exists.", + "dupSimilar": "A contact with a very similar number already exists.", + "loadingTags": "Loading tags...", + "noTagsAvailable": "No tags available. Create tags in Settings.", + "phoneRequired": "Phone number is required" + }, + "detailView": { + "tabs": { + "details": "Details", + "activity": "Activity", + "notes": "Notes", + "custom": "Custom Fields", + "deals": "Deals", + "tags": "Tags", + "noTags": "No tags", + "noDeals": "No deals", + "addNotePlaceholder": "Add a note..." + }, + "actions": { + "edit": "Edit", + "message": "Message", + "delete": "Delete" + }, + "contactInfo": "Contact Information", + "unnamed": "Unknown", + "contactDetailsDesc": "Contact details", + "phone": "Phone", + "email": "Email", + "company": "Company", + "createdAt": "Created At", + "customFields": "Custom Fields", + "noCustomFields": "No custom fields defined. Create them in Settings.", + "manageFields": "Manage fields", + "enterCustomField": "Enter {name}...", + "activityTab": { + "title": "Activity History", + "noActivity": "No activity yet." + }, + "notesTab": { + "title": "Internal Notes", + "placeholder": "Write a note...", + "save": "Add Note", + "noNotes": "No notes yet." + }, + "dealsTab": { + "noDeals": "No deals yet" + }, + "tagsTab": { + "clickTagDesc": "Click a tag to add or remove it from this contact.", + "noTagsAvailable": "No tags available. Create tags in Settings." + }, + "sendTemplateBtn": "Send template", + "saveChangesBtn": "Save Changes", + "saveCustomFieldsBtn": "Save Custom Fields", + "toastPhoneRequired": "Phone number is required", + "toastUpdateFailed": "Failed to update contact", + "toastUpdated": "Contact updated", + "toastNotAuthenticated": "Not authenticated", + "toastNoteAddFailed": "Failed to add note", + "toastNoteAdded": "Note added", + "toastNoteDeleteFailed": "Failed to delete note", + "toastNoteDeleted": "Note deleted", + "toastCustomFieldsSaved": "Custom fields saved", + "toastCustomFieldsFailed": "Failed to save custom fields", + "toastTemplateSent": "Template \"{name}\" sent", + "toastTemplateFailed": "Failed to send template: {reason}" + }, + "importModal": { + "title": "Import Contacts", + "desc": "Upload a CSV with a required phone column. Optional: name, email, company, tags (comma-separated; quote multi-tag cells).", + "uploadDropzone": "Click to choose a CSV file", + "uploadHint": ".csv up to your browser limit", + "rowsReady": "{count} row ready", + "rowsReady_plural": "{count} rows ready", + "preview": "Preview · first {count}", + "previewTags": "{tags} tag · {contacts} contact", + "previewTags_plural": "{tags} tags · {contacts} contacts", + "moreRows": "+ {count} more row not shown", + "moreRows_plural": "+ {count} more rows not shown", + "importComplete": "Import complete", + "resultImported": "{count} imported", + "resultTags": "{count} tag assigned", + "resultTags_plural": "{count} tags assigned", + "resultSkipped": "{count} skipped", + "resultFailed": "{count} failed", + "willBeCreated": "{name} (will be created on import)", + "columns": { + "phone": "Phone", + "name": "Name", + "email": "Email", + "company": "Company", + "tags": "Tags" + }, + "cancel": "Cancel", + "close": "Close", + "importBtn": "Import {count} contact", + "importBtn_plural": "Import {count} contacts", + "toastNoValidRows": "No valid rows found. Ensure CSV has a \"phone\" column header.", + "toastTagsWarning": "Contacts imported, but some tag assignments failed.", + "toastImported": "{count} contact imported", + "toastImported_plural": "{count} contacts imported", + "toastTagsAssigned": "{count} tag assignment applied", + "toastTagsAssigned_plural": "{count} tag assignments applied", + "toastTagsSkipped": "Unknown tags skipped (create them in Settings first): {sample}{more}", + "toastSkipped": "{count} duplicate skipped", + "toastSkipped_plural": "{count} duplicates skipped", + "toastFailed": "{count} contact failed to import", + "toastFailed_plural": "{count} contacts failed to import", + "toastError": "Import failed" + }, + "customFields": { + "title": "Custom fields", + "desc": "Define extra contact fields (e.g. ZIP code, lead source). They appear on every contact and in the “Update Contact Field” automation action.", + "addField": "Add", + "fieldName": "New field name…", + "loading": "Loading…", + "empty": "No custom fields yet.", + "renameAria": "Rename {name}", + "deleteTitle": "Delete field", + "deleteConfirm": "Delete \"{name}\"? This also removes its stored value on every contact. This cannot be undone.", + "toastNoAccount": "Your profile is not linked to an account.", + "toastDuplicate": "A field named \"{name}\" already exists.", + "toastCreateFailed": "Could not create field. You may not have permission.", + "toastCreated": "Created \"{name}\".", + "toastRenameFailed": "Could not rename field.", + "toastDeleteFailed": "Could not delete field.", + "toastDeleted": "Deleted \"{name}\"." + } + }, + "Pipelines": { + "page": { + "selectPipeline": "Select Pipeline", + "noPipelinesYet": "No pipelines yet", + "managePipelines": "Manage Pipelines", + "addPipeline": "Add Pipeline", + "addDeal": "Add Deal", + "createToStartTracking": "Create a pipeline to start tracking deals", + "createPipeline": "Create Pipeline", + "newPipeline": "New Pipeline", + "pipelineName": "Pipeline Name", + "pipelineNamePlaceholder": "e.g., Enterprise Sales", + "defaultStagesDesc": "Default stages (New Lead → Won) will be created automatically.", + "cancel": "Cancel", + "creating": "Creating...", + "createPipelineBtn": "Create Pipeline", + "toastFailedLoadPipelines": "Failed to load pipelines", + "toastFailedMoveDeal": "Failed to move deal", + "toastFailedCreatePipeline": "Failed to create pipeline", + "toastPipelineCreated": "Pipeline created", + "toastNotLinkedToAccount": "Your profile is not linked to an account." + }, + "board": { + "dropDealHere": "Drop a deal here", + "addDeal": "Add Deal" + }, + "card": { + "noContact": "No contact", + "won": "Won", + "lost": "Lost" + }, + "form": { + "editDeal": "Edit Deal", + "newDeal": "New Deal", + "title": "Title", + "titlePlaceholder": "Deal title", + "contact": "Contact", + "selectContact": "Select a contact", + "linkToConversation": "Link to Conversation", + "value": "Value", + "currency": "Currency", + "expectedCloseDate": "Expected Close Date", + "stage": "Stage", + "assignedTo": "Assigned To", + "unassigned": "Unassigned", + "notes": "Notes", + "notesPlaceholder": "Add notes...", + "status": "Status", + "markAsWon": "Mark as Won", + "markAsLost": "Mark as Lost", + "reopenDeal": "Reopen deal", + "cancel": "Cancel", + "saving": "Saving...", + "saveChanges": "Save Changes", + "createDeal": "Create Deal", + "deletePrompt": "Delete this deal?", + "confirm": "Confirm", + "deleting": "Deleting...", + "deleteDeal": "Delete Deal", + "toastRequired": "Title, contact, and stage are required", + "toastFailedSave": "Failed to save deal", + "toastNotSignedIn": "Not signed in", + "toastNotLinked": "Your profile is not linked to an account.", + "toastFailedCreate": "Failed to create deal", + "toastUpdated": "Deal updated", + "toastCreated": "Deal created", + "toastFailedStatus": "Failed to update deal status", + "toastMarkedWon": "Marked as won", + "toastMarkedLost": "Marked as lost", + "toastReopened": "Deal reopened", + "toastFailedDelete": "Failed to delete deal", + "toastDeleted": "Deal deleted" + }, + "settings": { + "managePipeline": "Manage Pipeline", + "deletePipeline": "Delete Pipeline", + "deletePipelineDesc": "This will archive all deals in this pipeline. This cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting...", + "deletePipelineBtn": "Delete Pipeline", + "pipelineName": "Pipeline Name", + "stages": "Stages", + "newStageNamePlaceholder": "New stage name", + "add": "Add", + "createNewPipeline": "Create a new pipeline", + "saving": "Saving...", + "saveChanges": "Save Changes", + "dragToReorder": "Drag to reorder", + "changeColor": "Change color", + "toastFailedSave": "Failed to save pipeline", + "toastSaved": "Pipeline saved", + "toastFailedAddStage": "Failed to add stage", + "toastMoveOrDeleteDeals": "Move or delete deals in this stage first", + "toastFailedDeleteStage": "Failed to delete stage", + "toastFailedDeletePipeline": "Failed to delete pipeline", + "toastDeleted": "Pipeline deleted" + }, + "analytics": { + "totalDeals": "Total Deals", + "totalDealsTooltip": "Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included.", + "pipelineValue": "Pipeline Value", + "pipelineValueTooltip": "Sum of the dollar values of all deals in this pipeline, excluding deals marked as Lost.", + "avgDealSize": "Avg Deal Size", + "avgDealSizeTooltip": "Pipeline Value divided by Total Deals — the average value of a single non-lost deal.", + "weightedValue": "Weighted Value", + "weightedValueTooltip": "Expected revenue: each open deal's value × its stage probability. First stage ≈ 10%, stages progress up to 90%, Won = 100%. Lost deals are excluded.", + "wonThisMonth": "Won This Month", + "wonThisMonthTooltip": "Deals marked as Won since the first day of the current month.", + "lostThisMonth": "Lost This Month", + "lostThisMonthTooltip": "Deals marked as Lost since the first day of the current month.", + "howCalculated": "How {label} is calculated" + } + }, + "Broadcasts": { + "page": { + "title": "Broadcasts", + "subtitle": "Send bulk messages to your contacts using approved templates.", + "newBroadcast": "New Broadcast", + "noBroadcastsYet": "No broadcasts yet", + "createFirst": "Create your first broadcast to reach your contacts at scale.", + "table": { + "name": "Name", + "template": "Template", + "recipients": "Recipients", + "delivery": "Delivery", + "read": "Read", + "status": "Status", + "date": "Date" + }, + "errorLoad": "Failed to load broadcasts", + "retry": "Retry" + }, + "status": { + "draft": "Draft", + "scheduled": "Scheduled", + "sending": "Sending", + "sent": "Sent", + "failed": "Failed", + "pending": "Pending", + "delivered": "Delivered", + "read": "Read", + "replied": "Replied", + "unknown": "Unknown" + }, + "detail": { + "backToBroadcasts": "Back to Broadcasts", + "notFound": "Broadcast not found", + "template": "Template: {name}", + "createdAt": "Created {date}", + "deletePrompt": "Delete this broadcast?", + "cancel": "Cancel", + "confirm": "Confirm", + "deleting": "Deleting…", + "delete": "Delete", + "cannotDeleteSending": "Cannot delete while a broadcast is actively sending", + "deleteHover": "Delete this broadcast", + "funnel": "Funnel", + "recipientsHeader": "Recipients ({filtered} of {total})", + "recipientsHeaderAll": "Recipients ({total})", + "allStatuses": "All statuses", + "exportCsv": "Export CSV", + "noRecipients": "No recipients found.", + "noRecipientsFilter": "No recipients match this filter.", + "table": { + "contact": "Contact", + "phone": "Phone", + "status": "Status", + "sent": "Sent", + "delivered": "Delivered", + "read": "Read", + "error": "Error" + }, + "stats": { + "totalRecipients": "Total Recipients", + "sent": "Sent", + "delivered": "Delivered", + "read": "Read", + "replied": "Replied", + "failed": "Failed" + }, + "toastFailedDelete": "Failed to delete: {error}", + "toastDeleted": "Broadcast deleted" + }, + "new": { + "title": "New Broadcast", + "subtitle": "Create and send a broadcast message to your contacts.", + "steps": { + "template": "Template", + "audience": "Audience", + "personalize": "Personalize", + "send": "Send" + }, + "toastGiveName": "Give the broadcast a name before saving a draft.", + "toastNotSignedIn": "Not signed in.", + "toastNotLinked": "Your profile is not linked to an account.", + "toastFailedDraft": "Failed to save draft: {error}", + "toastDraftSaved": "Draft saved" + }, + "wizard": { + "next": "Next", + "back": "Back", + "cancel": "Cancel", + "chooseTemplate": { + "title": "Choose a Template", + "subtitle": "Select an approved message template for your broadcast.", + "noTemplates": "No templates available.", + "createFirst": "Create a template in Settings first.", + "errorLoad": "Failed to load templates" + }, + "selectAudience": { + "title": "Select Audience", + "subtitle": "Choose who will receive this broadcast.", + "method": { + "all": "All Contacts", + "tags": "Filter by Tags", + "customField": "Custom Field", + "csv": "Upload CSV" + }, + "allDesc": "Send to all {count} contacts in your database.", + "allDescLoading": "Send to all contacts in your database.", + "tagDesc": "Send to contacts matching specific tags.", + "customFieldDesc": "Send to contacts matching a custom field rule.", + "csvDesc": "Upload a CSV with phone numbers (and optional names).", + "selectTags": "Select Tags", + "searchTags": "Search tags...", + "noTagsFound": "No tags found.", + "excludeTags": "Exclude Tags (Optional)", + "searchExcludeTags": "Search tags to exclude...", + "field": "Field", + "selectField": "Select a field...", + "operator": "Operator", + "operatorIs": "Is", + "operatorIsNot": "Is not", + "operatorContains": "Contains", + "value": "Value", + "valuePlaceholder": "Value to match", + "uploadCsv": "Upload CSV File", + "csvFormatDesc": "Must contain a 'phone' column (E.164 format). 'name' column is optional.", + "csvContactsFound": "{count} valid contact(s) found in CSV.", + "errorLoadTags": "Failed to load tags", + "errorLoadFields": "Failed to load custom fields", + "errorCsvParse": "Could not parse CSV file.", + "errorCsvMissingPhone": "CSV must have a 'phone' column." + }, + "personalize": { + "title": "Personalize Variables", + "subtitle": "Map your template variables to contact fields or provide static values.", + "headerImage": "Header Image", + "headerImageDesc": "This template requires a header image.", + "imageUrl": "Image URL", + "imageUrlPlaceholder": "https://example.com/image.jpg", + "variables": "Variables", + "varDesc": "Set values for {{name}}", + "type": "Type", + "typeStatic": "Static Value", + "typeContact": "Contact Field", + "typeCustom": "Custom Field", + "staticValue": "Static Value", + "contactField": "Contact Field", + "selectContactField": "Select field...", + "customField": "Custom Field", + "selectCustomField": "Select custom field...", + "preview": "Preview", + "previewSample": "Showing preview with sample data.", + "noPreview": "Preview not available.", + "fieldMap": { + "name": "Name", + "phone": "Phone", + "email": "Email" + } + }, + "scheduleSend": { + "title": "Review & Send", + "subtitle": "Give your broadcast a name and confirm sending.", + "broadcastName": "Broadcast Name", + "broadcastNamePlaceholder": "e.g., Summer Promo", + "summary": "Summary", + "template": "Template:", + "audience": "Audience:", + "audienceAll": "All contacts", + "audienceTags": "Filtered by tags", + "audienceField": "Filtered by custom field", + "audienceCsv": "CSV upload", + "variables": "Variables:", + "variablesMapped": "{count} mapped", + "saveDraft": "Save as Draft", + "sendNow": "Send Broadcast", + "sending": "Sending...", + "preparing": "Preparing...", + "progressDesc": "Processing {current} of {total} recipients...", + "toastFailedPreview": "Failed to load preview data" + } + } + }, + "Automations": { + "list": { + "title": "Automations", + "subtitle": "Build workflows that react to WhatsApp® events automatically.", + "create": "Create Automation", + "retry": "Retry", + "templatesTitle": "Quick-start templates", + "emptyTitle": "No automations yet", + "emptyDesc": "Pick a template above or create one from scratch.", + "runs": "{count} run", + "runsPlural": "{count} runs", + "lastRun": "last {time}", + "activate": "Activate", + "deactivate": "Deactivate", + "edit": "Edit", + "duplicate": "Duplicate", + "viewLogs": "View Logs", + "delete": "Delete", + "cancel": "Cancel", + "deleteTitle": "Delete automation", + "deleteDesc": "This permanently removes {name} and its execution history. This cannot be undone.", + "toasts": { + "activated": "Automation activated", + "paused": "Automation paused", + "duplicated": "Automation duplicated", + "deleted": "Automation deleted", + "updateError": "Failed to update", + "duplicateError": "Failed to duplicate", + "deleteError": "Failed to delete" + } + }, + "edit": { + "loading": "Loading...", + "loadError": "Failed to load ({status})", + "back": "Back to Automations" + }, + "logs": { + "title": "Execution logs", + "loadError": "Failed to load logs", + "emptyTitle": "No executions yet", + "emptyDesc": "Trigger this automation to see runs here.", + "unknownContact": "Unknown contact", + "step": "{count} step", + "stepPlural": "{count} steps", + "noSteps": "No steps recorded.", + "status": { + "success": "success", + "partial": "partial", + "failed": "failed" + }, + "back": "Back", + "backAria": "Back" + }, + "builder": { + "steps": { + "send_message": "Send Message", + "send_buttons": "Send Buttons", + "send_list": "Send List", + "send_template": "Send Template", + "add_tag": "Add Tag", + "remove_tag": "Remove Tag", + "assign_conversation": "Assign Conversation", + "update_contact_field": "Update Contact Field", + "create_deal": "Create Deal", + "wait": "Wait", + "condition": "Condition (If/Else)", + "send_webhook": "Send Webhook", + "close_conversation": "Close Conversation" + }, + "triggers": { + "new_message_received": { + "label": "New Message Received", + "hint": "Any incoming message" + }, + "first_inbound_message": { + "label": "First Message from Contact", + "hint": "First time this contact ever messages you (works for manually-added contacts too)" + }, + "keyword_match": { + "label": "Keyword Match", + "hint": "Message contains specific keyword(s)" + }, + "interactive_reply": { + "label": "Button / List Reply", + "hint": "Customer taps a button or list row whose id matches — chain menus across automations" + }, + "new_contact_created": { + "label": "New Contact Created", + "hint": "When a contact is auto-created from an incoming message" + }, + "conversation_assigned": { + "label": "Conversation Assigned", + "hint": "When assigned to an agent" + }, + "tag_added": { + "label": "Tag Added", + "hint": "When a tag is added to a contact" + }, + "time_based": { + "label": "Time-Based", + "hint": "On a recurring schedule" + } + }, + "trigger": "Trigger", + "triggerType": "Trigger type", + "keywords": "Keywords", + "keywordsHint": "Comma-separated list (e.g. hello, help, buy)", + "replyIds": "Reply IDs", + "replyIdsHint": "yes, no, more_info", + "replyIdsHelp": "Comma-separated button/list-row ids to match, exact. Use the same ids you set on a Send Buttons / Send List step.", + "schedule": "Schedule", + "scheduleHint": "Cron expression (e.g. 0 9 * * 1-5)", + "active": "Active", + "activeAria": "Active", + "save": "Save", + "saveDraft": "Save Draft", + "untitled": "Untitled automation", + "backToAutomations": "Back to automations", + "toasts": { + "saved": "Automation saved", + "created": "Automation created", + "saveFailed": "Save failed" + }, + "tags": { + "select": "Select a tag…", + "placeholder": "Tag id", + "unknown": "{id} (unknown tag)" + }, + "fields": { + "name": "Name", + "email": "Email", + "company": "Company", + "customFields": "Custom fields", + "unknown": "{id} (unknown field)" + }, + "agents": { + "select": "Select an agent…", + "placeholder": "Agent id", + "unknown": "{id} (unknown agent)" + }, + "pipelines": { + "pipelineLabel": "Pipeline", + "pipelineIdLabel": "Pipeline id", + "stageLabel": "Stage", + "stageIdLabel": "Stage id", + "selectPipeline": "Select a pipeline…", + "selectStage": "Select a stage…", + "selectPipelineFirst": "Select a pipeline first…", + "unknownPipeline": "{id} (unknown pipeline)", + "unknownStage": "{id} (unknown stage)" + }, + "templates": { + "templateLabel": "Template", + "templateNameLabel": "Template name", + "languageLabel": "Language", + "select": "Select a template…", + "unknown": "{name} ({lang}) — not in approved list", + "unknownLang": "unknown" + }, + "branches": { + "yes": "Yes", + "no": "No" + }, + "addStep": "Add step", + "config": { + "messageText": "Message text", + "placeholderMessageText": "Hi! Thanks for reaching out…", + "tagLabel": "Tag", + "modeLabel": "Mode", + "modes": { + "round_robin": "Round-robin", + "specific": "Specific agent" + }, + "agentLabel": "Agent", + "fieldLabel": "Field", + "valueLabel": "Value", + "placeholderValue": "Text or {{ vars.x }} / {{ message.text }}", + "titleLabel": "Title", + "amountLabel": "Amount", + "unitLabel": "Unit", + "units": { + "minutes": "Minutes", + "hours": "Hours", + "days": "Days" + }, + "subjectLabel": "Subject", + "subjects": { + "tag_presence": "Tag presence", + "contact_field": "Contact field", + "message_content": "Message content", + "time_of_day": "Time of day" + }, + "operandLabel": "Operand", + "placeholderTime": "HH:mm-HH:mm", + "placeholderContact": "name / email / company", + "placeholderTag": "tag id", + "urlLabel": "URL", + "headersLabel": "Headers", + "placeholderHeaders": "{\"Authorization\": \"Bearer ...\"}", + "bodyTemplateLabel": "Body template", + "placeholderBody": "{\"id\": \"{{ contact.id }}\"}", + "matchType": "Match type", + "matchContains": "Contains", + "matchExact": "Exact", + "closeConversationHint": "Sets the conversation status to \"closed\". No configuration needed." + } + } + }, + "Flows": { + "list": { + "title": "Flows", + "beta": "Beta", + "description": "Build branching, button-driven WhatsApp conversations. Useful for menus, FAQs, and triage before a human steps in.", + "newFlow": "New flow", + "statusDraft": "Draft", + "statusActive": "Active", + "statusArchived": "Archived", + "loadError": "Couldn't load flows.", + "createError": "Couldn't create flow.", + "cloneError": "Clone failed", + "deleteConfirm": "Delete \"{name}\"? Any active runs will end immediately.", + "deleteSuccess": "Flow deleted.", + "deleteError": "Couldn't delete flow.", + "emptyTitle": "No flows yet", + "emptyDesc": "Build your first conversation — a welcome menu, an order lookup, an FAQ bot. Customers tap buttons; the bot routes them to the right answer (or the right agent).", + "createFirst": "Create your first flow", + "createTitle": "Create a new flow", + "createDesc": "Start from a template or build from scratch.", + "startTemplate": "Start from a template", + "nodeCount": "{count} {count, plural, =1 {node} other {nodes}}", + "startBlank": "Or start blank", + "placeholderName": "e.g. Welcome menu", + "cancel": "Cancel", + "createBlank": "Create blank flow", + "runCount": "{count} {count, plural, =1 {run} other {runs}}", + "edit": "Edit", + "delete": "Delete", + "triggerKeywordNone": "Triggers on keyword (none set)", + "triggerKeyword": "Triggers on: {keywords}", + "triggerFirstInbound": "Triggers on a contact's first-ever inbound message", + "triggerManual": "Manual trigger" + }, + "edit": { + "notFound": "Flow not found.", + "backToFlows": "← Back to flows", + "loadError": "Couldn't load flow." + }, + "logs": { + "title": "Runs", + "description": "The 50 most recent times this flow ran. Expand a row to see the engine's per-step log.", + "emptyState": "No runs yet. Trigger the flow from a personal WhatsApp number to see it appear here.", + "loadError": "Couldn't load runs.", + "unknownContact": "Unknown contact", + "atNode": "at {node}", + "started": "Started {time}", + "reprompts": "{count} re-prompts", + "ranFor": "ran for {duration}", + "capturedVars": "Captured vars ({count})", + "noEvents": "No events recorded for this run.", + "statusActive": "Active", + "statusCompleted": "Completed", + "statusHandedOff": "Handed off", + "statusTimedOut": "Timed out", + "statusPaused": "Paused by agent", + "statusFailed": "Failed" + }, + "builder": { + "triggerTitle": "Trigger", + "whenLabel": "When…", + "triggerKeywordTitle": "A message contains a keyword", + "triggerFirstInboundTitle": "Customer's first ever inbound message", + "triggerManualTitle": "Manual only (no auto-trigger)", + "keywordsLabel": "Keywords (comma-separated)", + "keywordsPlaceholder": "support, help, hi", + "entryNodeTitle": "Entry node:", + "entryNodePlaceholder": "Pick the first node…", + "nodesTitle": "Nodes ({count})", + "nodesEmpty": "Add a Start node, then a Send buttons node, then a Handoff — that's the welcome-menu shape from the brief.", + "addNode": "Add node", + "setAsEntry": "Set as entry", + "removeNode": "Remove node", + "showAdvanced": "Show advanced", + "hideAdvanced": "Hide advanced", + "nodeKeyLabel": "Node key (internal identifier — keep stable for analytics)", + "replyIdsHint": "Reply IDs for each option are shown inline above. They're returned by WhatsApp when a customer taps; you usually don't need to touch them.", + "badgeEntry": "Entry", + "noNodesYet": "No nodes yet.", + "deleteNode": "Delete node", + "canvasView": "Canvas", + "listView": "List", + "noNodeSelected": "No node selected", + "noNodeDesc": "Select a node on the canvas to edit its configuration.", + "form": { + "advancesTo": "Advances to", + "textToCustomer": "Text sent to the customer", + "promptToCustomer": "Prompt sent to the customer", + "varKeyLabel": "Variable key (stored in flow_runs.vars; alphanumeric + underscore)", + "varKeyPlaceholder": "e.g. name, email, company", + "varKeyHelp": "Interpolate in downstream prompts and handoff notes with", + "advanceAfterCapture": "After capturing, advance to", + "internalNote": "Internal note (for the agent picking up)", + "endNodeHelp": "Terminal node. When the runner reaches this node the run is marked complete. No config needed.", + "bodyText": "Body text", + "footerText": "Footer (optional, 60 chars)", + "buttonsHelp": "Buttons (1–3) — each one routes to a different next node", + "optionTitlePlaceholder": "Visible title (≤20 chars)", + "nextNodePlaceholder": "Next node…", + "addButton": "Add button", + "buttonLabel": "Tap-to-expand button label (≤20 chars)", + "rowsHelp": "Rows (1–10 total across all sections)", + "sectionTitlePlaceholder": "Section {count} title (optional)", + "rowTitlePlaceholder": "Row title (≤24)", + "addRow": "Add row", + "addSection": "Add section", + "ifLabel": "If", + "capturedVariable": "Captured variable", + "contactHasTag": "Contact has tag", + "contactField": "Contact field", + "varName": "var name", + "tagLabel": "Tag", + "fieldLabel": "Field", + "pickTag": "Pick a tag…", + "pickField": "Pick a field…", + "operatorLabel": "Operator", + "isPresent": "is present", + "isAbsent": "is absent", + "equals": "equals", + "contains": "contains", + "valueLabel": "Value", + "ifTrueAdvance": "If true → advance to", + "ifFalseAdvance": "If false → advance to", + "actionLabel": "Action", + "addTag": "Add tag", + "removeTag": "Remove tag", + "tagUuidPlaceholder": "Tag UUID", + "thenAdvanceTo": "Then advance to", + "mediaTypeLabel": "Media type", + "imageLabel": "Image (PNG, JPEG, WebP)", + "videoLabel": "Video (MP4, 3GP)", + "documentLabel": "Document (PDF, Word, Excel, PowerPoint, TXT)", + "fileLabel": "File", + "removeFile": "Remove file", + "uploading": "Uploading…", + "clickToUpload": "Click to upload (max 16 MB)", + "captionLabel": "Caption (optional, shown under the media)", + "filenameLabel": "Filename shown to the customer (documents only)", + "filenamePlaceholder": "invoice.pdf", + "advanceAfterSending": "After sending, advance to", + "pickNextNode": "Pick a next node…", + "none": "— None —" + }, + "categories": { + "messaging": "Messaging", + "logic": "Logic & data", + "flow": "Flow control" + }, + "nodes": { + "start": { + "label": "Start", + "blurb": "Entry point of the flow" + }, + "send_message": { + "label": "Send message", + "blurb": "Sends a WhatsApp text message" + }, + "send_buttons": { + "label": "Send buttons", + "blurb": "Sends quick-reply buttons" + }, + "send_list": { + "label": "Send list", + "blurb": "Sends a tappable list of options" + }, + "send_media": { + "label": "Send media", + "blurb": "Sends an image, video, or document" + }, + "collect_input": { + "label": "Collect input", + "blurb": "Asks a question, saves the reply" + }, + "condition": { + "label": "If / else", + "blurb": "Branches on a rule" + }, + "set_tag": { + "label": "Tag contact", + "blurb": "Adds or removes a contact tag" + }, + "handoff": { + "label": "Handoff to agent", + "blurb": "Hands the conversation to a human" + }, + "end": { + "label": "End", + "blurb": "Ends the flow" + } + } + }, + "validation": { + "noIssues": "No issues. Ready to activate.", + "summary": "{errorCount} {errorCount, plural, =1 {error} other {errors}}, {warningCount} {warningCount, plural, =1 {warning} other {warnings}}", + "jumpToNode": "Jump to node {key}" + }, + "editorState": { + "saved": "Saved.", + "fixIssues": "Fix the issues below before activating.", + "statusActivated": "Flow activated.", + "statusArchived": "Archived.", + "statusDraft": "Saved as draft." + }, + "summary": { + "options": "{count} {count, plural, =1 {option} other {options}}", + "optionsAcrossSections": "{rowCount} {rowCount, plural, =1 {option} other {options}} across {sectionCount} {sectionCount, plural, =1 {section} other {sections}}", + "noFile": "{label} (no file uploaded)", + "media": "Media", + "image": "Image", + "video": "Video", + "document": "Document", + "audio": "Audio", + "hasTag": "has tag {tag}", + "opContains": "contains", + "opExists": "exists", + "opMissing": "missing", + "modeAdd": "Add", + "modeRemove": "Remove", + "tagPicked": "{mode} tag {tag}…", + "tagNone": "{mode} tag (none picked)" + } + }, + "Settings": { + "pageTitle": "Settings", + "pageDesc": "Everything in one place — your account and your workspace. Pick a section to manage it.", + "overview": { + "notSetup": "Not set up yet", + "connected": "Connected", + "needsReconnecting": "Needs reconnecting", + "viewTeamMembers": "View team members", + "membersCount": "{count} {count, plural, =1 {member} other {members}}", + "pendingInvites": "{count} pending {count, plural, =1 {invite} other {invites}}", + "manageTemplates": "Manage message templates", + "templatesCount": "{count} {count, plural, =1 {template} other {templates}}", + "pendingReview": "{count} pending review", + "tagsAndFields": "Tags and custom fields", + "tagsCount": "{count} {count, plural, =1 {tag} other {tags}}", + "fieldsCount": "{count} custom {count, plural, =1 {field} other {fields}}", + "appearance": "{mode} mode · {theme} accent", + "yourAccount": "Your account", + "loading": "Loading…" + }, + "members": { + "title": "Team members", + "description": "People with access to this account. Roles control what each teammate can do.", + "inviteMember": "Invite member", + "online": "online", + "away": "away", + "offline": "offline", + "memberCount": "{count} {count, plural, =1 {member} other {members}}", + "you": "You", + "unnamed": "Unnamed", + "joined": "Joined {date}", + "remove": "Remove", + "removedToast": "Removed {name}", + "updatedToast": "Updated {name} to {role}", + "pendingInvitations": "Pending invitations", + "inviteHint": "The plaintext invite URL is only shown once at creation for security — to re-share, revoke the invite below and create a new one.", + "noPendingTitle": "No pending invitations.", + "noPendingDesc": "Click Invite member above to generate a shareable link.", + "untitledInvite": "Untitled invite", + "created": "Created {date}", + "expiresInDays": "expires in {days} {days, plural, =1 {day} other {days}}", + "expiresInHours": "expires in {hours} {hours, plural, =1 {hour} other {hours}}", + "expired": "expired", + "revoke": "Revoke", + "revokedToast": "Invitation revoked", + "removeDialogTitle": "Remove member", + "removeDialogDesc": "Remove {name} from the account? They'll be signed out of this account and given a fresh personal account on their next sign-in. Their login isn't deleted.", + "cancel": "Cancel", + "removing": "Removing...", + "removeBtn": "Remove member" + }, + "invite": { + "inviteCreated": "Invite created", + "inviteCreatedDesc": "Share this link with your new teammate. They'll be able to sign up (or sign in) and join the account as {role}. The link is valid for {days} {days, plural, =1 {day} other {days}}.", + "inviteLink": "Invite link", + "copy": "Copy", + "saveLinkNow": "Save this link now.", + "saveLinkHint": "We never store the plaintext — once you close this dialog the URL is gone. To re-share, revoke this invite and create a new one.", + "sendViaWhatsApp": "Send via WhatsApp", + "done": "Done", + "dialogTitle": "Invite a teammate", + "dialogDesc": "Generate a one-time invite link. Share it via WhatsApp, Slack, or any channel you like — no email service required.", + "roleLabel": "Role", + "validForLabel": "Link valid for", + "days1": "1 day", + "days7": "7 days", + "days30": "30 days", + "labelTitle": "Label", + "optional": "(optional)", + "labelPlaceholder": "e.g. Sara — support team", + "labelHint": "Helps you remember who you sent the link to in the pending list below.", + "cancel": "Cancel", + "creating": "Creating...", + "generateLink": "Generate link", + "labelTooLong": "Label must be {max} characters or fewer", + "copied": "Invite link copied", + "clipboardBlocked": "Clipboard blocked — copy the link manually", + "whatsappMessage": "Join {accountName} on waivy using this link (valid for {expiresInDays} days): {url}" + }, + "tagsAndFields": { + "title": "Fields & tags", + "description": "Two ways to organize contacts: colour-coded tags for quick grouping, and custom fields for structured data.", + "tagsTitle": "Tags", + "tagsDesc": "Colour-coded labels for grouping and filtering contacts.", + "fieldsTitle": "Custom fields", + "fieldsDesc": "Extra contact fields (e.g. ZIP code, lead source). They appear on every contact and in the “Update Contact Field” automation action.", + "adminRole": "Admin", + "failedToLoadTags": "Failed to load tags", + "nameRequired": "Tag name is required", + "notAuthenticated": "Not authenticated", + "tagCreated": "Tag created", + "failedToCreateTag": "Failed to create tag", + "tagDeleted": "Tag deleted", + "failedToDeleteTag": "Failed to delete tag", + "deleteAria": "Delete {name}", + "noTags": "No tags yet — create your first one below.", + "placeholder": "e.g. Newsletter", + "useColor": "Use {color}", + "addTag": "Add tag", + "deleteTag": "Delete tag", + "deleteConfirm": "Delete the tag \"{name}\"? This removes it from all contacts and cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting...", + "colors": { + "red": "Red", + "orange": "Orange", + "amber": "Amber", + "emerald": "Emerald", + "cyan": "Cyan", + "blue": "Blue", + "violet": "Violet", + "pink": "Pink" + } + }, + "templates": { + "title": "Message templates", + "description": "Create templates and submit them to Meta for approval. Use \"Sync from Meta\" to pull templates approved elsewhere.", + "syncFromMeta": "Sync from Meta", + "syncing": "Syncing…", + "syncTitle": "Pull approved templates from your Meta WhatsApp Business Account", + "newTemplate": "New Template", + "noTemplates": "No templates yet.", + "createFirst": "Create your first message template to get started.", + "editTitle": "Editing triggers Meta re-review — status flips to PENDING.", + "editLabel": "Edit template", + "edit": "Edit", + "resubmitTitle": "Edit the template and resubmit to Meta for review.", + "resubmitLabel": "Edit and resubmit template", + "resubmit": "Resubmit", + "deleteMetaLocallyAria": "Delete template from Meta and locally", + "deleteLocallyAria": "Delete template locally", + "deleteMetaLocallyTitle": "Delete from Meta and locally", + "deleteLocallyTitle": "Delete locally", + + "dialogEditTitle": "Edit Message Template", + "dialogNewTitle": "New Message Template", + "dialogEditDesc": "Save your changes to re-submit to Meta. Status will flip back to PENDING during review.", + "dialogNewDesc": "Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.", + "authWarning": "AUTHENTICATION templates have a fixed body + OTP button shape that needs a different builder. Create them in Meta WhatsApp Manager for now and use Sync from Meta to bring them in.", + + "templateName": "Template Name", + "namePlaceholder": "e.g. order_confirmation", + "nameFixed": "Name is fixed once a template exists on Meta — create a new template to change it.", + "nameHint": "Lowercase letters, digits, and underscores only.", + "category": "Category", + "language": "Language", + "langFixed": "Language is fixed once a template exists on Meta.", + "langHint": "Must match the exact code on Meta — en_US and en are distinct.", + + "header": "Header", + "headerNone": "None", + "headerText": "Text", + "headerImage": "Image", + "headerVideo": "Video", + "headerDocument": "Document", + "headerTextPlaceholder": "Header text (max 60 chars, optional {{1}})", + "headerSampleAria": "Sample value for header variable", + "headerSamplePlaceholder": "Sample value for {{1}} (required for Meta review)", + + "uploadImage": "Upload image", + "uploadHint": "JPEG or PNG, ≤5 MB", + "mediaUrlPlaceholder": "https://… (or paste a public {format} link)", + "imageHint": "Upload a JPEG/PNG (≤5 MB, ≥800×418 px recommended) or paste a public HTTPS link — we upload it to Meta for review automatically.", + "mediaHint": "Must be a publicly accessible HTTPS link. Meta fetches it once during review, so it needs to stay live for ~24 hrs.", + "videoHint": " Recommended: MP4 / 3GPP, ≤16 MB, ≤60 seconds.", + "documentHint": " Recommended: PDF, ≤100 MB.", + + "bodyText": "Body Text", + "bodyPlaceholder": "Hello {{1}}, your order {{2}} is confirmed.", + "bodyHint": "Use {{1}}, {{2}} for variables (must be contiguous starting at {{1}}).", + "sampleValues": "Sample values (Meta uses these to review your template)", + "sampleAria": "Sample value for body variable {var}", + "samplePlaceholder": "Sample for {var}", + + "footer": "Footer (optional)", + "footerPlaceholder": "Optional footer text (max 60 chars)", + + "buttons": "Buttons (optional)", + "addButton": "Add Button", + "buttonsLimit": "Up to {max} buttons. QUICK_REPLY buttons must come before URL / phone / copy-code buttons.", + "btnQuickReply": "Quick Reply", + "btnUrl": "URL", + "btnPhone": "Phone", + "btnCopyCode": "Copy Code", + "btnLabelPlaceholder": "Button label", + "urlPlaceholder": "https://example.com/path or with {{1}} suffix", + "urlSamplePlaceholder": "Example value for {{1}} (required when URL has a variable)", + "phonePlaceholder": "+15551234567", + "codePlaceholder": "Example code (e.g. SUMMER20)", + + "cancel": "Cancel", + "saving": "Saving…", + "submitting": "Submitting…", + "saveResubmit": "Save & Resubmit", + "submitApproval": "Submit for Approval", + + "deleteDialogTitle": "Delete template?", + "deleteMetaDesc": "\"{name}\" will be deleted from Meta and from waivy. Active broadcasts using this template will start failing on their next send. This can't be undone.", + "deleteLocalDesc": "\"{name}\" will be deleted from waivy. It was never submitted to Meta, so no remote cleanup is needed.", + "deleting": "Deleting…", + "delete": "Delete", + + "toastLoadFailed": "Failed to load templates", + "toastSubmitFailed": "Failed to submit", + "toastSubmitEditSuccess": "Edit submitted — Meta typically reviews within 24 hours.", + "toastSubmitNewSuccess": "Submitted to Meta — typical review time is 24 hours. Status updates automatically.", + "toastSaveEditDry": "Template updated (dry-run — no Meta call)", + "toastSaveNewDry": "Template saved (dry-run — no Meta call)", + "toastSyncCount": "Synced {total} {total, plural, =1 {template} other {templates}} from Meta", + "toastSyncDetails": " ({inserted} new, {updated} updated)", + "toastSyncFailed": "Failed to sync: {preview}", + "toastSyncTruncated": "Synced the first 2000 templates only — your account has more. Sync again to continue, or contact support if this persists.", + "toastSyncError": "Failed to sync templates", + "toastDeleteSuccess": "Template deleted", + "toastDeleteError": "Failed to delete template", + "toastInvalidImage": "Header image must be a JPEG or PNG.", + "toastImageTooLarge": "Image is {size} MB — Meta's limit is 5 MB.", + "toastUploadSuccess": "Image uploaded.", + "toastUploadFailed": "Upload failed." + }, + "roles": { + "admin": "Admin", + "adminHint": "Manage members + everything", + "agent": "Agent", + "agentHint": "Use features; no settings", + "viewer": "Viewer", + "viewerHint": "Read-only across the app", + "owner": "Owner", + "ownerHint": "Full control over account and billing" + }, + "whatsapp": { + "title": "WhatsApp connection", + "description": "Connect your Meta WhatsApp Business API. Credentials, webhook, and setup steps all live here.", + "tokenCorrupted": "Stored token can't be decrypted", + "resetting": "Resetting...", + "resetConfig": "Reset Configuration", + "credentialsValid": "Credentials valid", + "notConnected": "Not Connected", + "connectedDesc": "Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.", + "notConnectedDesc": "Configure your Meta API credentials below to connect your WhatsApp Business account.", + "registered": "Registered — Meta will deliver events to waivy", + "notRegistered": "Not registered — Meta will not deliver events", + "verifyWithMeta": "Verify with Meta", + "subscribedSince": "Subscribed since {date}. Click Verify with Meta if events stop arriving.", + "unknownDate": "unknown", + "lastAttemptFailed": "Last attempt failed with: ", + "retryHint": "Enter (or correct) the 2-step PIN below and click Save Configuration to retry.", + "noRegistrationHint": "This number was saved before registration tracking existed, or registration was skipped. Enter the 2-step PIN below and click Save Configuration to subscribe it.", + "diagnosticLastRun": "Diagnostic — last run: ", + "live": "live", + "notLive": "not live", + "apiCredentialsTitle": "API Credentials", + "apiCredentialsDesc": "Enter your Meta WhatsApp Business API credentials.", + "phoneNumberId": "Phone Number ID", + "wabaId": "WhatsApp Business Account ID", + "accessToken": "Permanent Access Token", + "accessTokenPlaceholder": "Enter your access token", + "tokenHidden": "Token is hidden for security. Re-enter it to update configuration.", + "webhookVerifyToken": "Webhook Verify Token", + "webhookVerifyTokenPlaceholder": "Create a custom verify token", + "webhookVerifyTokenHint": "A custom string you create. Must match the token you set in Meta webhook settings.", + "twoStepPin": "Two-step verification PIN", + "optional": "(optional)", + "pinPlaceholder": "6-digit PIN from Meta WhatsApp Manager", + "pinHint": "Needed only to wire inbound messages for a production number. Set it in Meta Business Manager → WhatsApp Accounts → Phone Numbers → Two-step verification, then paste it here so waivy can subscribe the number — otherwise Meta routes inbound events to whichever app last claimed it (the symptom that hits second numbers under a shared WABA). Meta test numbers have no PIN and are pre-registered — leave this blank for them. Leaving it blank also keeps an existing registration untouched.", + "webhookTitle": "Webhook Configuration", + "webhookDesc": "Use this URL as your webhook callback in the Meta App Dashboard.", + "webhookUrl": "Webhook Callback URL", + "saving": "Saving...", + "saveConfig": "Save Configuration", + "testing": "Testing...", + "testConnection": "Test API Connection", + "setupInstructions": "Setup Instructions", + "setupInstructionsDesc": "Follow these steps to connect your WhatsApp Business API.", + "step1": "Create a Meta App", + "step1_1": "Go to developers.facebook.com", + "step1_2": "Click \"My Apps\" and then \"Create App\"", + "step1_3": "Select \"Business\" as the app type", + "step1_4": "Fill in app details and create", + "step2": "Add WhatsApp Product", + "step2_1": "In your app dashboard, click \"Add Product\"", + "step2_2": "Find \"WhatsApp\" and click \"Set Up\"", + "step2_3": "Follow the setup wizard to link your business", + "step3": "Get API Credentials", + "step3_1": "Go to WhatsApp > API Setup", + "step3_2": "Copy your Phone Number ID", + "step3_3": "Copy your WhatsApp Business Account ID", + "step3_4": "Generate a Permanent Access Token from Business Settings > System Users", + "step4": "Configure Webhooks", + "step4_1": "Go to WhatsApp > Configuration", + "step4_2": "Click \"Edit\" on the Webhook section", + "step4_3": "Paste the Webhook Callback URL from above", + "step4_4": "Enter the same Verify Token you set here", + "step4_5": "Subscribe to \"messages\" webhook field", + "metaDocs": "Meta WhatsApp API Documentation" + }, + "sections": { + "overview": "Overview", + "profile": "Your profile", + "security": "Login & security", + "appearance": "Appearance", + "whatsapp": "WhatsApp", + "templates": "Templates", + "quick-replies": "Quick replies", + "fields": "Fields & tags", + "deals": "Deals & currency", + "members": "Team members", + "api": "API keys" + }, + "groups": { + "account": "Account", + "workspace": "Workspace" + }, + "profile": { + "title": "Your profile", + "description": "How you show up across the app. Your avatar and name appear in the header, sidebar, and anywhere your teammates see you.", + "changePhoto": "Change photo", + "uploadPhoto": "Upload photo", + "remove": "Remove", + "photoHint": "PNG, JPG, WebP, or GIF. Up to 2 MB.", + "displayName": "Display name", + "email": "Email", + "emailChangeHint": "Check the inbox for {oldEmail} and {newEmail} — both need to confirm before the change takes effect.", + "accountDetails": "Account details", + "role": "Role", + "joined": "Joined", + "userId": "User ID", + "loading": "Loading your profile…", + "saving": "Saving…", + "saveChanges": "Save changes", + "unsupportedImage": "Unsupported image type", + "unsupportedImageDesc": "Use PNG, JPG, WebP, or GIF.", + "imageTooLarge": "Image is too large", + "imageTooLargeDesc": "Maximum 2 MB.", + "nameRequired": "Display name is required", + "invalidEmail": "Enter a valid email address", + "uploadFailed": "Upload failed: {message}", + "saveFailed": "Save failed: {message}", + "profileSaved": "Profile saved", + "emailChangeFailed": "Email change failed: {message}", + "profileSavedEmailCheck": "Profile saved — check your email to confirm the address change", + + "passwordTitle": "Password", + "passwordDesc": "Use at least {min} characters. You will stay signed in on this device after changing it.", + "currentPassword": "Current password", + "newPassword": "New password", + "confirmPassword": "Confirm new password", + "updating": "Updating…", + "updatePassword": "Update password", + "cannotChangeNoEmail": "Cannot change password without a current email", + "passwordTooShort": "Password must be at least {min} characters", + "passwordMismatch": "New password and confirmation do not match", + "currentPasswordIncorrect": "Current password is incorrect", + "passwordUpdateFailed": "Password update failed: {message}", + "passwordUpdated": "Password updated", + + "sessionsTitle": "Active sessions", + "sessionsDesc": "Sign out of every device where you're logged in — including this one. Useful if you lost a laptop or shared your password.", + "signOutAll": "Sign out of all devices", + "signOutConfirmTitle": "Sign out everywhere?", + "signOutConfirmDesc": "Every device logged into this account will be signed out and will need to log in again. You will be redirected to the login page.", + "cancel": "Cancel", + "signingOut": "Signing out…", + "signOutEverywhere": "Sign out everywhere", + "signOutFailed": "Sign-out failed: {message}" + }, + "appearance": { + "title": "Appearance", + "description": "Set the mode and accent colour used across the app. Saved to this device — try it, it changes live.", + "mode": "Mode", + "useMode": "Use {mode} mode", + "active": "Active", + "accentColor": "Accent color", + "useTheme": "Use {name} theme" + }, + "security": { + "title": "Login & security", + "description": "Change your password and sign out of your devices. These keep your account safe." + }, + "apiKeys": { + "title": "API keys", + "description": "Keys authenticate the public REST API (/api/v1) so you can build your own automations. Send them as Authorization: Bearer <key>.", + "newApiKey": "New API key", + "noApiKeys": "No API keys yet.", + "createOneHint": "Click New API key to create one.", + "askAdminHint": "Ask an admin to create one.", + "revoked": "Revoked", + "expired": "Expired", + "noScopes": "No scopes", + "created": "Created {date}", + "lastUsed": "last used {date}", + "neverUsed": "never used", + "expires": "expires {date}", + "revoke": "Revoke", + "revokeSuccess": "Revoked \"{name}\"", + "revokeFailed": "Failed to revoke key", + "loadFailed": "Failed to load API keys", + "createError": "Failed to create key", + "nameRequired": "Give the key a name", + "nameLabel": "Name", + "namePlaceholder": "e.g. Zapier automation", + "scopesLabel": "Scopes", + "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.", + "cancel": "Cancel", + "creating": "Creating…", + "createKey": "Create key", + "copyTitle": "Copy your API key", + "copyDesc": "This is the only time the full key is shown. Store it somewhere safe — if you lose it, revoke it and create a new one.", + "apiKeyLabel": "API key", + "copy": "Copy", + "copySuccess": "API key copied", + "copyFailed": "Copy failed — select and copy manually", + "newKeyTitle": "New API key", + "newKeyDesc": "Name it after the integration that will use it, and grant only the scopes it needs.", + "done": "Done", + "networkError": "Could not reach the server" + }, + "deals": { + "title": "Deals & currency", + "description": "The currency used for new deals and for pipeline and dashboard totals.", + "defaultCurrency": "Default currency", + "defaultCurrencyDesc": "New deals default to this currency, and pipeline and dashboard totals are shown in it. Existing deals keep the currency they were saved with.", + "currencyLabel": "Currency", + "adminOnlyHint": "Only account admins can change the default currency.", + "save": "Save", + "saving": "Saving...", + "saveFailed": "Failed to save default currency", + "saveSuccess": "Default currency updated" + }, + "aiConfig": { + "title": "Agent setup", + "description": "Bring your own OpenAI or Anthropic key waivy calls the provider directly with your key — no per-seat AI fees, and your data stays yours. This powers AI-drafted replies in the inbox, the auto-reply bot, and the Playground.", + "adminOnlyConfig": "Only admins and owners can change the AI configuration.", + "loadFailed": "Failed to load AI configuration", + "providerAndKey": "Provider & key", + "encryptionNotice": "Your key is encrypted at rest (AES-256-GCM) and never shown again after saving.", + "provider": "Provider", + "model": "Model", + "apiKey": "API key", + "testKey": "Test key", + "testSuccess": "Key works — the provider responded.", + "testRejected": "The provider rejected the request.", + "testNetworkError": "Could not reach the provider.", + "embeddingsKey": "Embeddings key", + "optionalSemanticSearch": "(optional — enables semantic knowledge-base search)", + "embeddingsHint": "An OpenAI key used only to embed your knowledge base (text-embedding-3-small){sameKeyText}. Leave blank to use keyword search instead. Clear it to turn semantic search off.", + "sameKeyText": " — can be the same key as above", + "behaviour": "Behaviour", + "behaviourDesc": "Tell the assistant about your business — products, tone, what it may and may not promise. This context feeds both drafts and auto-replies.", + "businessContext": "Business context & instructions", + "promptPlaceholder": "e.g. We are Acme, a coffee-equipment store. Be warm and concise. Never quote prices or delivery dates — hand off to a human for those.", + "enableAssistant": "Enable AI assistant", + "enableAssistantDesc": "Master switch. Turns on the “Draft with AI” button in the inbox.", + "autoReply": "Auto-reply to inbound messages", + "autoReplyDesc": "The bot answers new inbound messages automatically (only when no flow handles them and no agent is assigned). Hands off to a human when it can’t help.", + "maxAutoReplies": "Max auto-replies per conversation", + "maxAutoRepliesDesc": "After this many bot replies in one thread, the bot goes quiet.", + "handoffTo": "Hand off to", + "handoffToDesc": "When the bot can’t help — or hits the reply cap — it pauses and routes the chat here, with a short note on what happened.", + "handoffQueue": "Unassigned queue (any agent can pick it up)", + "remove": "Remove", + "save": "Save", + "missingModel": "Enter a model name.", + "missingApiKey": "Enter your API key.", + "saveSuccess": "AI assistant saved.", + "saveFailed": "Failed to save.", + "removeSuccess": "AI configuration removed.", + "removeFailed": "Failed to remove." + }, + "aiKnowledge": { + "loadFailed": "Failed to load knowledge base", + "openFailed": "Failed to open document", + "titleContentRequired": "Title and content are required.", + "saveSuccessNew": "Document added.", + "saveSuccessUpdate": "Document updated.", + "saveFailed": "Failed to save.", + "removeSuccess": "Document removed.", + "removeFailed": "Failed to remove.", + "reindexSuccess": "Reindexed {count} document(s).", + "reindexFailed": "Reindex failed.", + "title": "Knowledge base", + "description": "Add FAQs, policies, or product details. The assistant retrieves the relevant pieces when drafting and auto-replying, so it can answer instead of handing off.{searchType}", + "semanticSearchOn": " Semantic search is on (embeddings key set).", + "keywordSearchOn": " Using keyword search — add an embeddings key above for semantic search.", + "noDocs": "No documents yet.", + "editDocTitle": "Title", + "editDocTitlePlaceholder": "e.g. Returns & refunds policy", + "editDocContent": "Content", + "editDocContentPlaceholder": "Paste the FAQ answer, policy text, or product details…", + "cancel": "Cancel", + "saveDoc": "Save document", + "addDoc": "Add document", + "reindex": "Reindex", + "reindexTooltip": "Re-embed all documents (e.g. after adding an embeddings key)", + "loading": "Loading…" + } + } +} diff --git a/package-lock.json b/package-lock.json index 92ddfffc44..4772361ea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8660,17 +8660,6 @@ } } }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", diff --git a/package.json b/package.json index 07be10b830..7eeb29909b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "wacrm", + "name": "waivy", "version": "0.8.0", "private": true, - "description": "Self-hostable CRM template for WhatsApp built on Next.js and Supabase — shared inbox, contacts, sales pipelines, broadcasts, and no-code automations.", + "description": "waivy — CRM inteligente para WhatsApp. Bandeja compartida, contactos, pipeline de ventas, broadcasts, automaciones y agente de IA.", "license": "MIT", "author": "Arnas Donauskas", "homepage": "https://github.com/ArnasDon/wacrm", diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index abef3c2d39..926af50298 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -8,20 +8,8 @@ import { createClient } from "@/lib/supabase/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { MessageSquare, UsersRound } from "lucide-react"; - -// `useSearchParams` opts the component out of static prerendering -// unless it sits under a Suspense boundary. We split the form into -// a child component so the outer page can prerender the chrome -// (background, card frame) while the form hydrates with the query -// string on the client. +import { MessageSquare, Eye, EyeOff } from "lucide-react"; + export default function LoginPage() { return ( @@ -32,16 +20,15 @@ export default function LoginPage() { function LoginPageInner() { const searchParams = useSearchParams(); - // Forwarded from `/join/` when the visitor already has an - // account. After a successful sign-in we send them to the join - // page to accept rather than to /dashboard. const inviteToken = searchParams.get("invite"); const t = useTranslations("LoginPage"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [remember, setRemember] = useState(false); const supabase = createClient(); const handleLogin = async (e: React.FormEvent) => { @@ -60,14 +47,6 @@ function LoginPageInner() { return; } - // Full-page navigation (not router.push) so the browser issues a - // fresh top-level request that carries the just-written Supabase - // auth cookies to the middleware gating /dashboard. A soft - // client-side navigation can reach the protected route before the - // server observes the new session, so the middleware bounces it - // back to /login — which looks like the page "just refreshing" - // instead of signing in (issue #365). Mirrors the deliberate full - // reload the invite-accept flow already uses in join/[token]. const destination = inviteToken ? `/join/${encodeURIComponent(inviteToken)}` : "/dashboard"; @@ -75,35 +54,129 @@ function LoginPageInner() { }; return ( -

- - -
- {inviteToken ? ( - - ) : ( - - )} +
+ {/* Left side — Branding */} +
+ {/* Gradient background */} +
+ + {/* Abstract shapes */} +
+
+
+ + {/* Glowing envelope/wave shape */} +
+
+ {/* Outer glow ring */} +
+ {/* Envelope icon */} +
+ + {/* Envelope body */} + + {/* Envelope flap */} + + {/* Paper lines inside */} + + + + {/* Glow particles */} + + + + + + + + + + + + + + + + + +
+
+
+ + {/* Logo */} +
+
+ +
+ waivy +
+ + {/* Bottom text */} +
+

+ Atiende más. Vende más. Crece más. +

+

+ CRM inteligente para WhatsApp con agente de IA que atiende a tus + clientes 24/7, solo o con equipo humano. +

+
+ + {/* Navigation dots */} +
+
+
+
+
+
+ + {/* Right side — Login form */} +
+
+ {/* Top sign in button (mobile) */} +
+
+ waivy +
- + + {/* Title */} +

{inviteToken ? t('titleAccept') : t('titleWelcome')} - - +

+

{inviteToken ? t('descAccept') : t('descWelcome')} - - - -

- {error && ( -
- {error} -
- )} +

+ + {/* Error */} + {error && ( +
+ {error} +
+ )} -
-
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4a8c05a344..b6c66ef6b6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,10 +22,10 @@ const inter = Inter({ export const metadata: Metadata = { title: { - default: "wacrm", - template: "%s — wacrm", + default: "waivy", + template: "%s — waivy", }, - description: "Self-hostable CRM template for WhatsApp.", + description: "waivy — CRM inteligente para WhatsApp.", robots: { index: false, follow: false, diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000000..ad9264f0b1 --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000000..666d49e710 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,414 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "wacria" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 +# Controls whether new tables, views, sequences and functions created in the `public` schema by +# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) +# without explicit GRANTs. When unset, new entities are NOT auto-exposed, matching the new cloud +# default. Set to `true` to keep the legacy behaviour of auto-exposing new entities; this is +# deprecated and the field is removed on 2026-10-30 once the always-revoked behaviour is permanent. +# auto_expose_new_tables = true + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files, directories, or glob patterns that describe your database. +# Supports paths relative to supabase directory: "./schemas/*.sql", "./database". +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[local_smtp] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +[storage.vector] +enabled = true +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ `{{ .Code }}` }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ `{{ .Code }}` }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# pg-delta is the schema diff engine for db diff / db pull / db remote commit. +# Set enabled = false to fall back to the legacy migra engine. +[experimental.pgdelta] +enabled = true +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./database" +# JSON string passed through to pg-delta SQL formatting. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql index 835782a5c9..cf21159058 100644 --- a/supabase/migrations/001_initial_schema.sql +++ b/supabase/migrations/001_initial_schema.sql @@ -11,7 +11,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- PROFILES -- ============================================================ CREATE TABLE IF NOT EXISTS profiles ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, full_name TEXT NOT NULL, email TEXT NOT NULL, @@ -34,7 +34,7 @@ CREATE POLICY "Users can insert own profile" ON profiles FOR INSERT WITH CHECK ( -- CONTACTS -- ============================================================ CREATE TABLE IF NOT EXISTS contacts ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, phone TEXT NOT NULL, name TEXT, @@ -56,7 +56,7 @@ CREATE POLICY "Users can manage own contacts" ON contacts FOR ALL USING (auth.ui -- TAGS -- ============================================================ CREATE TABLE IF NOT EXISTS tags ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#3b82f6', @@ -71,7 +71,7 @@ CREATE POLICY "Users can manage own tags" ON tags FOR ALL USING (auth.uid() = us -- CONTACT_TAGS (many-to-many) -- ============================================================ CREATE TABLE IF NOT EXISTS contact_tags ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), @@ -90,7 +90,7 @@ CREATE POLICY "Users can manage contact tags" ON contact_tags FOR ALL -- CUSTOM_FIELDS -- ============================================================ CREATE TABLE IF NOT EXISTS custom_fields ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, field_name TEXT NOT NULL, field_type TEXT NOT NULL DEFAULT 'text', @@ -106,7 +106,7 @@ CREATE POLICY "Users can manage own custom fields" ON custom_fields FOR ALL USIN -- CONTACT_CUSTOM_VALUES -- ============================================================ CREATE TABLE IF NOT EXISTS contact_custom_values ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, custom_field_id UUID NOT NULL REFERENCES custom_fields(id) ON DELETE CASCADE, value TEXT, @@ -123,7 +123,7 @@ CREATE POLICY "Users can manage custom values" ON contact_custom_values FOR ALL -- CONTACT_NOTES -- ============================================================ CREATE TABLE IF NOT EXISTS contact_notes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, note_text TEXT NOT NULL, @@ -138,7 +138,7 @@ CREATE POLICY "Users can manage own notes" ON contact_notes FOR ALL USING (auth. -- CONVERSATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS conversations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'pending', 'closed')), @@ -161,7 +161,7 @@ CREATE POLICY "Users can manage own conversations" ON conversations FOR ALL USIN -- MESSAGES -- ============================================================ CREATE TABLE IF NOT EXISTS messages ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_type TEXT NOT NULL CHECK (sender_type IN ('customer', 'agent', 'bot')), sender_id UUID, @@ -188,7 +188,7 @@ CREATE POLICY "Service role can insert messages" ON messages FOR INSERT WITH CHE -- WHATSAPP_CONFIG -- ============================================================ CREATE TABLE IF NOT EXISTS whatsapp_config ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, phone_number_id TEXT NOT NULL, waba_id TEXT, @@ -209,7 +209,7 @@ CREATE POLICY "Users can manage own config" ON whatsapp_config FOR ALL USING (au -- MESSAGE_TEMPLATES -- ============================================================ CREATE TABLE IF NOT EXISTS message_templates ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'Marketing' CHECK (category IN ('Marketing', 'Utility', 'Authentication')), @@ -232,7 +232,7 @@ CREATE POLICY "Users can manage own templates" ON message_templates FOR ALL USIN -- PIPELINES -- ============================================================ CREATE TABLE IF NOT EXISTS pipelines ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() @@ -246,7 +246,7 @@ CREATE POLICY "Users can manage own pipelines" ON pipelines FOR ALL USING (auth. -- PIPELINE_STAGES -- ============================================================ CREATE TABLE IF NOT EXISTS pipeline_stages ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, @@ -265,7 +265,7 @@ CREATE POLICY "Users can manage pipeline stages" ON pipeline_stages FOR ALL -- DEALS -- ============================================================ CREATE TABLE IF NOT EXISTS deals ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE, stage_id UUID NOT NULL REFERENCES pipeline_stages(id), @@ -292,7 +292,7 @@ CREATE POLICY "Users can manage own deals" ON deals FOR ALL USING (auth.uid() = -- BROADCASTS -- ============================================================ CREATE TABLE IF NOT EXISTS broadcasts ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, template_name TEXT NOT NULL, @@ -319,7 +319,7 @@ CREATE POLICY "Users can manage own broadcasts" ON broadcasts FOR ALL USING (aut -- BROADCAST_RECIPIENTS -- ============================================================ CREATE TABLE IF NOT EXISTS broadcast_recipients ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'delivered', 'read', 'replied', 'failed')), diff --git a/supabase/migrations/006_automations.sql b/supabase/migrations/006_automations.sql index a15940fe65..4b18e91481 100644 --- a/supabase/migrations/006_automations.sql +++ b/supabase/migrations/006_automations.sql @@ -12,7 +12,7 @@ -- AUTOMATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS automations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, @@ -51,7 +51,7 @@ CREATE TRIGGER set_updated_at BEFORE UPDATE ON automations -- 'yes' or 'no' identifying which path. -- ============================================================ CREATE TABLE IF NOT EXISTS automation_steps ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, parent_step_id UUID REFERENCES automation_steps(id) ON DELETE CASCADE, branch TEXT CHECK (branch IN ('yes', 'no')), @@ -85,7 +85,7 @@ CREATE POLICY "Users can manage steps of own automations" ON automation_steps FO -- on broadcast_recipients / deals). -- ============================================================ CREATE TABLE IF NOT EXISTS automation_logs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, @@ -117,7 +117,7 @@ CREATE POLICY "Users can view own automation logs" ON automation_logs FOR ALL -- the engine uses the service-role client. No user policy exposed. -- ============================================================ CREATE TABLE IF NOT EXISTS automation_pending_executions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, diff --git a/supabase/migrations/009_message_actions.sql b/supabase/migrations/009_message_actions.sql index 323c59b4bf..1ca410b27e 100644 --- a/supabase/migrations/009_message_actions.sql +++ b/supabase/migrations/009_message_actions.sql @@ -40,7 +40,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_reply_to -- 2. message_reactions -- ============================================================ CREATE TABLE IF NOT EXISTS message_reactions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, actor_type TEXT NOT NULL CHECK (actor_type IN ('customer', 'agent')), diff --git a/supabase/migrations/010_flows.sql b/supabase/migrations/010_flows.sql index bd9a82a318..7f9f87ca60 100644 --- a/supabase/migrations/010_flows.sql +++ b/supabase/migrations/010_flows.sql @@ -75,7 +75,7 @@ ALTER TABLE messages -- 2. flows -- ============================================================ CREATE TABLE IF NOT EXISTS flows ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, @@ -112,7 +112,7 @@ CREATE POLICY "Users can manage own flows" ON flows FOR ALL -- 3. flow_nodes -- ============================================================ CREATE TABLE IF NOT EXISTS flow_nodes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flow_id UUID NOT NULL REFERENCES flows(id) ON DELETE CASCADE, node_key TEXT NOT NULL, node_type TEXT NOT NULL CHECK (node_type IN ( @@ -154,7 +154,7 @@ CREATE POLICY "Users manage nodes on their flows" ON flow_nodes FOR ALL -- 4. flow_runs -- ============================================================ CREATE TABLE IF NOT EXISTS flow_runs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flow_id UUID NOT NULL REFERENCES flows(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- contact_id intentionally SET NULL on delete (matches the @@ -214,7 +214,7 @@ CREATE POLICY "Users see own flow runs" ON flow_runs FOR SELECT -- 5. flow_run_events -- ============================================================ CREATE TABLE IF NOT EXISTS flow_run_events ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flow_run_id UUID NOT NULL REFERENCES flow_runs(id) ON DELETE CASCADE, event_type TEXT NOT NULL CHECK (event_type IN ( 'started', diff --git a/supabase/migrations/017_account_sharing.sql b/supabase/migrations/017_account_sharing.sql index fae7cf37e5..3884df219b 100644 --- a/supabase/migrations/017_account_sharing.sql +++ b/supabase/migrations/017_account_sharing.sql @@ -58,7 +58,7 @@ END $$; -- ACCOUNTS -- ============================================================ CREATE TABLE IF NOT EXISTS accounts ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, -- owner_user_id is denormalised for fast "is this user the owner of -- their account" reads and for the one-account-per-user invariant @@ -88,7 +88,7 @@ CREATE TRIGGER set_updated_at BEFORE UPDATE ON accounts -- once by the POST endpoint at creation time and never persisted. -- ============================================================ CREATE TABLE IF NOT EXISTS account_invitations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, role account_role_enum NOT NULL CHECK (role <> 'owner'), diff --git a/supabase/migrations/027_notifications.sql b/supabase/migrations/027_notifications.sql index 475f33d064..357c87f188 100644 --- a/supabase/migrations/027_notifications.sql +++ b/supabase/migrations/027_notifications.sql @@ -2,7 +2,7 @@ -- NOTIFICATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS notifications ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- Recipient — the agent this notification is for. user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, diff --git a/supabase/migrations/035_interactive_messages.sql b/supabase/migrations/035_interactive_messages.sql index 7dcfbd7d1a..6816b7f884 100644 --- a/supabase/migrations/035_interactive_messages.sql +++ b/supabase/migrations/035_interactive_messages.sql @@ -22,7 +22,7 @@ ALTER TABLE messages -- 2. Quick replies -------------------------------------------- CREATE TABLE IF NOT EXISTS quick_replies ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Tenancy. Every member of the account shares its quick replies. account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- Author / audit only — never used for tenancy isolation.