57 changed files with 2268 additions and 297 deletions
-
96docs/openapi.yaml
-
170readme.md
-
20src/cmd/root.go
-
25src/config/settings.go
-
7src/domains/message/message.go
-
2src/domains/send/audio.go
-
1src/domains/send/image.go
-
5src/domains/send/presence.go
-
1src/domains/send/send.go
-
9src/domains/user/account.go
-
1src/domains/user/user.go
-
21src/go.mod
-
33src/go.sum
-
46src/internal/rest/helpers/flushChatCsv.go
-
41src/internal/rest/message.go
-
22src/internal/rest/send.go
-
19src/internal/rest/user.go
-
96src/pkg/utils/chat_storage.go
-
76src/pkg/utils/general.go
-
88src/pkg/utils/general_test.go
-
5src/pkg/whatsapp/init.go
-
111src/pkg/whatsapp/utils.go
-
10src/pkg/whatsapp/webhook.go
-
14src/services/app.go
-
27src/services/message.go
-
158src/services/send.go
-
60src/services/user.go
-
15src/validations/message_validation.go
-
75src/validations/send_validation.go
-
187src/validations/send_validation_test.go
-
282src/views/assets/app.css
-
16src/views/components/AccountAvatar.js
-
113src/views/components/AccountChangeAvatar.js
-
1src/views/components/AccountPrivacy.js
-
16src/views/components/AccountUserInfo.js
-
1src/views/components/AppLogin.js
-
5src/views/components/AppLoginWithCode.js
-
1src/views/components/AppLogout.js
-
1src/views/components/AppReconnect.js
-
20src/views/components/GroupCreate.js
-
23src/views/components/GroupJoinWithLink.js
-
17src/views/components/GroupManageParticipants.js
-
21src/views/components/MessageDelete.js
-
18src/views/components/MessageReact.js
-
18src/views/components/MessageRevoke.js
-
18src/views/components/MessageUpdate.js
-
37src/views/components/SendAudio.js
-
25src/views/components/SendContact.js
-
37src/views/components/SendFile.js
-
80src/views/components/SendImage.js
-
26src/views/components/SendLocation.js
-
46src/views/components/SendMessage.js
-
25src/views/components/SendPoll.js
-
86src/views/components/SendPresence.js
-
64src/views/components/SendVideo.js
-
25src/views/components/generic/FormRecipient.js
-
102src/views/index.html
@ -0,0 +1,5 @@ |
|||||
|
package send |
||||
|
|
||||
|
type PresenceRequest struct { |
||||
|
Type string `json:"type" form:"type"` |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
package helpers |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
||||
|
"github.com/sirupsen/logrus" |
||||
|
) |
||||
|
|
||||
|
var flushMutex sync.Mutex |
||||
|
|
||||
|
func FlushChatCsv() error { |
||||
|
flushMutex.Lock() |
||||
|
defer flushMutex.Unlock() |
||||
|
|
||||
|
// Create an empty file (truncating any existing content)
|
||||
|
file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// StartAutoFlushChatStorage starts a goroutine that periodically flushes the chat storage
|
||||
|
func StartAutoFlushChatStorage() { |
||||
|
interval := time.Duration(config.AppChatFlushIntervalDays) * 24 * time.Hour |
||||
|
|
||||
|
go func() { |
||||
|
ticker := time.NewTicker(interval) |
||||
|
defer ticker.Stop() |
||||
|
|
||||
|
for range ticker.C { |
||||
|
if err := FlushChatCsv(); err != nil { |
||||
|
logrus.Errorf("Error flushing chat storage: %v", err) |
||||
|
} else { |
||||
|
logrus.Info("Successfully flushed chat storage") |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
logrus.Infof("Auto flush for chat storage started (your account chat still safe). Will flush every %d days", config.AppChatFlushIntervalDays) |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
package utils |
||||
|
|
||||
|
import ( |
||||
|
"encoding/csv" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"sync" |
||||
|
|
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
||||
|
) |
||||
|
|
||||
|
type RecordedMessage struct { |
||||
|
MessageID string `json:"message_id,omitempty"` |
||||
|
JID string `json:"jid,omitempty"` |
||||
|
MessageContent string `json:"message_content,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// mutex to prevent concurrent file access
|
||||
|
var fileMutex sync.Mutex |
||||
|
|
||||
|
func FindRecordFromStorage(messageID string) (RecordedMessage, error) { |
||||
|
fileMutex.Lock() |
||||
|
defer fileMutex.Unlock() |
||||
|
|
||||
|
file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644) |
||||
|
if err != nil { |
||||
|
return RecordedMessage{}, fmt.Errorf("failed to open storage file: %w", err) |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
reader := csv.NewReader(file) |
||||
|
records, err := reader.ReadAll() |
||||
|
if err != nil { |
||||
|
return RecordedMessage{}, fmt.Errorf("failed to read CSV records: %w", err) |
||||
|
} |
||||
|
|
||||
|
for _, record := range records { |
||||
|
if len(record) == 3 && record[0] == messageID { |
||||
|
return RecordedMessage{ |
||||
|
MessageID: record[0], |
||||
|
JID: record[1], |
||||
|
MessageContent: record[2], |
||||
|
}, nil |
||||
|
} |
||||
|
} |
||||
|
return RecordedMessage{}, fmt.Errorf("message ID %s not found in storage", messageID) |
||||
|
} |
||||
|
|
||||
|
func RecordMessage(messageID string, senderJID string, messageContent string) error { |
||||
|
fileMutex.Lock() |
||||
|
defer fileMutex.Unlock() |
||||
|
|
||||
|
message := RecordedMessage{ |
||||
|
MessageID: messageID, |
||||
|
JID: senderJID, |
||||
|
MessageContent: messageContent, |
||||
|
} |
||||
|
|
||||
|
// Read existing messages
|
||||
|
var records [][]string |
||||
|
if file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644); err == nil { |
||||
|
defer file.Close() |
||||
|
reader := csv.NewReader(file) |
||||
|
records, err = reader.ReadAll() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to read existing records: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Check for duplicates
|
||||
|
for _, record := range records { |
||||
|
if len(record) == 3 && record[0] == messageID { |
||||
|
return nil // Skip if duplicate found
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Prepare the new record
|
||||
|
newRecord := []string{message.MessageID, message.JID, message.MessageContent} |
||||
|
records = append([][]string{newRecord}, records...) // Prepend new message
|
||||
|
|
||||
|
// Write all records back to file
|
||||
|
file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to open file for writing: %w", err) |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
writer := csv.NewWriter(file) |
||||
|
defer writer.Flush() |
||||
|
|
||||
|
if err := writer.WriteAll(records); err != nil { |
||||
|
return fmt.Errorf("failed to write CSV records: %w", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
@ -0,0 +1,282 @@ |
|||||
|
:root { |
||||
|
--primary-color: #00A884; /* WhatsApp's new brand green */ |
||||
|
--secondary-color: #008069; /* WhatsApp's darker green */ |
||||
|
--tertiary-color: #075E54; /* WhatsApp's darkest green */ |
||||
|
--background-color: #EFEAE2; /* WhatsApp's authentic background */ |
||||
|
--card-hover-color: #ffffff; |
||||
|
--text-color: #111B21; /* WhatsApp's text color */ |
||||
|
--gradient-start: #00A884; |
||||
|
--gradient-end: #008069; |
||||
|
--message-background: #FFFFFF; |
||||
|
--success-green: #00A884; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
background-color: var(--background-color) !important; |
||||
|
color: var(--text-color) !important; |
||||
|
font-family: "Helvetica Neue", "Helvetica Neue", Helvetica, Arial, sans-serif !important; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
padding: 2em 1em !important; |
||||
|
max-width: 1400px !important; |
||||
|
margin: 0 auto !important; |
||||
|
} |
||||
|
|
||||
|
.main-header { |
||||
|
background: var(--primary-color) !important; |
||||
|
color: white !important; |
||||
|
padding: 1.5em 2em !important; |
||||
|
border-radius: 24px !important; |
||||
|
margin-bottom: 2em !important; |
||||
|
box-shadow: |
||||
|
0 20px 40px rgba(0, 168, 132, 0.2), |
||||
|
inset 0 0 80px rgba(255, 255, 255, 0.15) !important; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
transition: all 0.3s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
.main-header::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: |
||||
|
radial-gradient( |
||||
|
circle at top right, |
||||
|
rgba(255,255,255,0.2) 0%, |
||||
|
rgba(255,255,255,0) 60% |
||||
|
), |
||||
|
linear-gradient( |
||||
|
45deg, |
||||
|
rgba(255,255,255,0.1) 0%, |
||||
|
rgba(255,255,255,0) 70% |
||||
|
); |
||||
|
opacity: 0.8; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.main-header::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: -50%; |
||||
|
left: -50%; |
||||
|
right: -50%; |
||||
|
bottom: -50%; |
||||
|
background: |
||||
|
radial-gradient( |
||||
|
circle, |
||||
|
rgba(255,255,255,0.1) 0%, |
||||
|
transparent 70% |
||||
|
); |
||||
|
animation: rotate 20s linear infinite; |
||||
|
z-index: 0; |
||||
|
} |
||||
|
|
||||
|
@keyframes rotate { |
||||
|
from { transform: rotate(0deg); } |
||||
|
to { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
.main-header .ui.header { |
||||
|
position: relative; |
||||
|
z-index: 2; |
||||
|
margin: 0 !important; |
||||
|
font-weight: 700 !important; |
||||
|
font-size: clamp(1.5em, 3vw, 1.8em) !important; |
||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
letter-spacing: 0.5px; |
||||
|
} |
||||
|
|
||||
|
.main-header .title-container { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.main-header .whatsapp.icon { |
||||
|
font-size: 1em !important; |
||||
|
animation: float 4s ease-in-out infinite; |
||||
|
backdrop-filter: blur(5px); |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.main-header .whatsapp.icon:hover { |
||||
|
transform: scale(1.1); |
||||
|
background: rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.main-header .version-label { |
||||
|
margin-top: 4px !important; |
||||
|
background: rgba(255,255,255,0.12); |
||||
|
color: white; |
||||
|
font-size: 0.5em; |
||||
|
padding: 4px 10px; |
||||
|
border-radius: 12px; |
||||
|
backdrop-filter: blur(4px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.15); |
||||
|
transition: all 0.25s ease; |
||||
|
letter-spacing: 0.5px; |
||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
||||
|
} |
||||
|
|
||||
|
.main-header .version-label:hover { |
||||
|
background: rgba(255,255,255,0.2); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
@media (min-width: 768px) { |
||||
|
.main-header { |
||||
|
padding: 2em !important; |
||||
|
} |
||||
|
|
||||
|
.main-header .ui.header { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.main-header .version-label { |
||||
|
margin-top: 4px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes float { |
||||
|
0% { transform: translateY(0px) rotate(0deg); } |
||||
|
50% { transform: translateY(-8px) rotate(5deg); } |
||||
|
100% { transform: translateY(0px) rotate(0deg); } |
||||
|
} |
||||
|
|
||||
|
.ui.header { |
||||
|
font-weight: 600 !important; |
||||
|
color: var(--text-color) !important; |
||||
|
} |
||||
|
|
||||
|
.ui.cards > .card { |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05) !important; |
||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; |
||||
|
border-radius: 16px !important; |
||||
|
margin: 0.7em !important; |
||||
|
background-color: var(--message-background) !important; |
||||
|
border: 1px solid rgba(18, 140, 126, 0.1); |
||||
|
} |
||||
|
|
||||
|
.ui.cards > .card:hover { |
||||
|
transform: translateY(-8px) !important; |
||||
|
box-shadow: 0 12px 24px rgba(18, 140, 126, 0.15) !important; |
||||
|
background-color: var(--card-hover-color) !important; |
||||
|
} |
||||
|
|
||||
|
.ui.horizontal.divider { |
||||
|
font-size: 1.3em !important; |
||||
|
color: var(--secondary-color) !important; |
||||
|
margin: 2.5em 0 !important; |
||||
|
font-weight: 700 !important; |
||||
|
text-transform: uppercase; |
||||
|
letter-spacing: 1px; |
||||
|
} |
||||
|
|
||||
|
.ui.success.message { |
||||
|
border-radius: 16px !important; |
||||
|
box-shadow: 0 4px 8px rgba(37, 211, 102, 0.1) !important; |
||||
|
border: 1px solid rgba(37, 211, 102, 0.2) !important; |
||||
|
background-color: rgba(231, 247, 232, 0.8) !important; |
||||
|
color: var(--success-green) !important; |
||||
|
animation: slideIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
||||
|
backdrop-filter: blur(10px); |
||||
|
} |
||||
|
|
||||
|
@keyframes slideIn { |
||||
|
from { transform: translateX(-30px); opacity: 0; } |
||||
|
to { transform: translateX(0); opacity: 1; } |
||||
|
} |
||||
|
|
||||
|
.ui.button { |
||||
|
border-radius: 12px !important; |
||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; |
||||
|
/* background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%) !important; */ |
||||
|
background: var(--primary-color) !important; |
||||
|
color: white !important; |
||||
|
font-weight: 600 !important; |
||||
|
letter-spacing: 0.5px; |
||||
|
} |
||||
|
|
||||
|
.ui.button:hover { |
||||
|
transform: translateY(-3px) !important; |
||||
|
box-shadow: 0 8px 16px rgba(0, 168, 132, 0.2) !important; |
||||
|
filter: brightness(0.95); |
||||
|
} |
||||
|
|
||||
|
.ui.form input, .ui.form textarea { |
||||
|
border-radius: 12px !important; |
||||
|
border: 2px solid rgba(18, 140, 126, 0.1) !important; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.ui.form input:focus, .ui.form textarea:focus { |
||||
|
border-color: var(--primary-color) !important; |
||||
|
box-shadow: 0 0 0 3px rgba(0, 168, 132, 0.1) !important; |
||||
|
} |
||||
|
|
||||
|
.ui.toast-container { |
||||
|
padding: 1.5em !important; |
||||
|
} |
||||
|
|
||||
|
.ui.toast { |
||||
|
border-radius: 16px !important; |
||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important; |
||||
|
background-color: var(--message-background) !important; |
||||
|
border: 1px solid rgba(18, 140, 126, 0.1); |
||||
|
} |
||||
|
|
||||
|
.ui.grid { |
||||
|
margin: -0.7em !important; |
||||
|
} |
||||
|
|
||||
|
.ui.grid > .column { |
||||
|
padding: 0.7em !important; |
||||
|
} |
||||
|
|
||||
|
@keyframes fadeIn { |
||||
|
from { opacity: 0; transform: translateY(30px); } |
||||
|
to { opacity: 1; transform: translateY(0); } |
||||
|
} |
||||
|
|
||||
|
.ui.cards { |
||||
|
animation: fadeIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
||||
|
} |
||||
|
|
||||
|
.ui.modal { |
||||
|
border-radius: 20px !important; |
||||
|
background-color: var(--message-background) !important; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.ui.modal > .header { |
||||
|
border-radius: 20px 20px 0 0 !important; |
||||
|
background: var(--gradient-start) !important; |
||||
|
color: white !important; |
||||
|
padding: 1.5em !important; |
||||
|
} |
||||
|
|
||||
|
.ui.modal > .actions { |
||||
|
border-radius: 0 0 20px 20px !important; |
||||
|
background-color: rgba(248, 248, 248, 0.8) !important; |
||||
|
backdrop-filter: blur(10px); |
||||
|
} |
||||
|
|
||||
|
.ui.modal > .actions > .ui.button { |
||||
|
background: var(--primary-color) !important; |
||||
|
color: white !important; |
||||
|
font-weight: 600 !important; |
||||
|
letter-spacing: 0.5px; |
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
export default { |
||||
|
name: 'AccountChangeAvatar', |
||||
|
data() { |
||||
|
return { |
||||
|
loading: false, |
||||
|
selected_file: null, |
||||
|
preview_url: null |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
openModal() { |
||||
|
$('#modalChangeAvatar').modal({ |
||||
|
onApprove: function () { |
||||
|
return false; |
||||
|
} |
||||
|
}).modal('show'); |
||||
|
}, |
||||
|
isValidForm() { |
||||
|
return this.selected_file !== null; |
||||
|
}, |
||||
|
async handleSubmit() { |
||||
|
if (!this.isValidForm() || this.loading) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
let response = await this.submitApi() |
||||
|
showSuccessInfo(response) |
||||
|
$('#modalChangeAvatar').modal('hide'); |
||||
|
} catch (err) { |
||||
|
showErrorInfo(err) |
||||
|
} |
||||
|
}, |
||||
|
async submitApi() { |
||||
|
this.loading = true; |
||||
|
try { |
||||
|
let payload = new FormData(); |
||||
|
payload.append('avatar', $("#file_avatar")[0].files[0]) |
||||
|
|
||||
|
let response = await window.http.post(`/user/avatar`, payload) |
||||
|
this.handleReset(); |
||||
|
return response.data.message; |
||||
|
} catch (error) { |
||||
|
if (error.response) { |
||||
|
throw new Error(error.response.data.message); |
||||
|
} |
||||
|
throw new Error(error.message); |
||||
|
} finally { |
||||
|
this.loading = false; |
||||
|
} |
||||
|
}, |
||||
|
handleReset() { |
||||
|
this.preview_url = null; |
||||
|
this.selected_file = null; |
||||
|
$("#file_avatar").val(''); |
||||
|
}, |
||||
|
handleImageChange(event) { |
||||
|
const file = event.target.files[0]; |
||||
|
if (file) { |
||||
|
this.preview_url = URL.createObjectURL(file); |
||||
|
this.selected_file = file.name; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
template: `
|
||||
|
<div class="blue card" @click="openModal()" style="cursor:pointer;"> |
||||
|
<div class="content"> |
||||
|
<a class="ui olive right ribbon label">Account</a> |
||||
|
<div class="header">Change Avatar</div> |
||||
|
<div class="description"> |
||||
|
Update your profile picture |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Modal Change Avatar --> |
||||
|
<div class="ui small modal" id="modalChangeAvatar"> |
||||
|
<i class="close icon"></i> |
||||
|
<div class="header"> |
||||
|
Change Avatar |
||||
|
</div> |
||||
|
<div class="content" style="max-height: 70vh; overflow-y: auto;"> |
||||
|
<div class="ui warning message"> |
||||
|
<i class="info circle icon"></i> |
||||
|
Please upload a square image (1:1 aspect ratio) to avoid cropping. |
||||
|
For best results, use an image at least 400x400 pixels. |
||||
|
</div> |
||||
|
|
||||
|
<form class="ui form"> |
||||
|
<div class="field" style="padding-bottom: 30px"> |
||||
|
<label>Avatar Image</label> |
||||
|
<input type="file" style="display: none" id="file_avatar" accept="image/png,image/jpg,image/jpeg" @change="handleImageChange"/> |
||||
|
<label for="file_avatar" class="ui positive medium green left floated button" style="color: white"> |
||||
|
<i class="ui upload icon"></i> |
||||
|
Upload image |
||||
|
</label> |
||||
|
<div v-if="preview_url" style="margin-top: 60px"> |
||||
|
<img :src="preview_url" style="max-width: 100%; max-height: 300px; object-fit: contain" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
<div class="actions"> |
||||
|
<button class="ui approve positive right labeled icon button" |
||||
|
:class="{'loading': this.loading, 'disabled': !isValidForm() || loading}" |
||||
|
@click.prevent="handleSubmit"> |
||||
|
Update Avatar |
||||
|
<i class="save icon"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
`
|
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
export default { |
||||
|
name: 'SendPresence', |
||||
|
data() { |
||||
|
return { |
||||
|
type: 'available', |
||||
|
loading: false, |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
openModal() { |
||||
|
$('#modalSendPresence').modal({ |
||||
|
onApprove: function () { |
||||
|
return false; |
||||
|
} |
||||
|
}).modal('show'); |
||||
|
}, |
||||
|
async handleSubmit() { |
||||
|
if (this.loading) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
let response = await this.submitApi() |
||||
|
showSuccessInfo(response) |
||||
|
$('#modalSendPresence').modal('hide'); |
||||
|
} catch (err) { |
||||
|
showErrorInfo(err) |
||||
|
} |
||||
|
}, |
||||
|
async submitApi() { |
||||
|
this.loading = true; |
||||
|
try { |
||||
|
let payload = { |
||||
|
type: this.type |
||||
|
} |
||||
|
let response = await window.http.post(`/send/presence`, payload) |
||||
|
return response.data.message; |
||||
|
} catch (error) { |
||||
|
if (error.response) { |
||||
|
throw new Error(error.response.data.message); |
||||
|
} |
||||
|
throw new Error(error.message); |
||||
|
} finally { |
||||
|
this.loading = false; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
template: `
|
||||
|
<div class="blue card" @click="openModal()" style="cursor: pointer"> |
||||
|
<div class="content"> |
||||
|
<a class="ui blue right ribbon label">Send</a> |
||||
|
<div class="header">Send Presence</div> |
||||
|
<div class="description"> |
||||
|
Set <div class="ui green horizontal label">available</div> or <div class="ui grey horizontal label">unavailable</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Modal SendPresence --> |
||||
|
<div class="ui small modal" id="modalSendPresence"> |
||||
|
<i class="close icon"></i> |
||||
|
<div class="header"> |
||||
|
Send Presence |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<form class="ui form"> |
||||
|
<div class="field"> |
||||
|
<label>Presence Status</label> |
||||
|
<select v-model="type" class="ui dropdown"> |
||||
|
<option value="available">Available</option> |
||||
|
<option value="unavailable">Unavailable</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
<div class="actions"> |
||||
|
<button class="ui approve positive right labeled icon button" |
||||
|
:class="{'loading': loading, 'disabled': loading}" |
||||
|
@click.prevent="handleSubmit"> |
||||
|
Send |
||||
|
<i class="send icon"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
`
|
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue