Issues (4)

app/client/client.go (1 issue)

Severity
1
package client
2
3
import (
4
	"fmt"
5
	"github.com/gen2brain/beeep"
6
	"github.com/popstas/planfix-go/planfix"
7
	"github.com/viasite/planfix-toggl-server/app/config"
8
	"github.com/viasite/planfix-toggl-server/app/util"
9
	"io/ioutil"
10
	"log"
11
	"math"
12
	"net/smtp"
13
	"reflect"
14
	"regexp"
15
	"sort"
16
	"strconv"
17
	"strings"
18
	"time"
19
)
20
21
type TogglSession interface {
22
	GetAccount() (toggl.Account, error)
23
	AddRemoveTag(entryID int, tag string, add bool) (toggl.TimeEntry, error)
24
	GetCurrentTimeEntry() (toggl.TimeEntry, error)
25
	GetSummaryReport(workspace int, since, until string) (toggl.SummaryReport, error)
26
	GetDetailedReport(workspace int, since, until string, page int) (toggl.DetailedReport, error)
27
	GetDetailedReportV2(rp toggl.DetailedReportParams) (toggl.DetailedReport, error)
28
	GetTagByName(name string, wid int) (tag toggl.Tag, err error)
29
	GetWorkspaces() (workspaces []toggl.Workspace, err error)
30
}
31
32
// TogglClient - Клиент, общающийся с Toggl и Планфиксом
33
type TogglClient struct {
34
	Session      TogglSession
35
	Config       *config.Config
36
	PlanfixAPI   planfix.API
37
	Logger       *log.Logger
38
	analiticData PlanfixAnaliticData
39
	SentLog      map[string]int
40
	Opts         map[string]string
41
}
42
43
// PlanfixEntryData - Данные, подмешивающиеся к toggl.DetailedTimeEntry
44
type PlanfixEntryData struct {
45
	Sent       bool `json:"sent"`
46
	TaskID     int  `json:"task_id"`
47
	GroupCount int  `json:"group_count"`
48
}
49
50
// TogglPlanfixEntry - toggl.DetailedTimeEntry дополнительными данными о задаче в Планфиксе
51
type TogglPlanfixEntry struct {
52
	toggl.DetailedTimeEntry
53
	Planfix PlanfixEntryData `json:"planfix"`
54
}
55
56
// TogglPlanfixEntryGroup - группа toggl-записей, объединенных одной задачей Планфикса
57
type TogglPlanfixEntryGroup struct {
58
	Entries         []TogglPlanfixEntry
59
	Description     string
60
	Project         string
61
	ProjectHexColor string
62
	Duration        int64
63
}
64
65
// PlanfixAnaliticData - данные аналитики, которая будет проставляться в Планфикс
66
type PlanfixAnaliticData struct {
67
	ID          int
68
	TypeID      int
69
	TypeValueID int
70
	CountID     int
71
	CommentID   int
72
	DateID      int
73
	UsersID     int
74
}
75
76
// Run запускает фоновые процессы
77
func (c TogglClient) Run() {
78
	// init map
79
	// start tag cleaner
80
	go c.RunTagCleaner()
81
	// start sender
82
	go c.RunSender()
83
}
84
85
// RunSender - запускалка цикла отправки накопившихся toggl-записей
86
func (c TogglClient) RunSender() {
87
	if c.Config.SendInterval <= 0 {
88
		c.Logger.Println("[INFO] Интервал отправки не установлен, периодическая отправка отключена")
89
		return
90
	}
91
92
	time.Sleep(1 * time.Second) // wait for server start
93
	for {
94
		c.SendToPlanfix()
95
		time.Sleep(time.Duration(c.Config.SendInterval) * time.Minute)
96
	}
97
}
98
99
// ReloadConfig - пересоздает API из последней версии конфига
100
func (c *TogglClient) ReloadConfig() {
101
	c.PlanfixAPI = planfix.New(
102
		c.Config.PlanfixAPIUrl,
103
		c.Config.PlanfixAPIKey,
104
		c.Config.PlanfixAccount,
105
		c.Config.PlanfixUserName,
106
		c.Config.PlanfixUserPassword,
107
	)
108
	if !c.Config.Debug {
109
		c.PlanfixAPI.Logger.SetFlags(0)
110
		c.PlanfixAPI.Logger.SetOutput(ioutil.Discard)
111
	}
112
	c.PlanfixAPI.UserAgent = "planfix-toggl"
113
114
	sess := toggl.OpenSession(c.Config.TogglAPIToken)
115
	c.Session = &sess
116
}
117
118
// RunTagCleaner - запускалка цикла очистки запущенных toggl-записей от тега sent
119
func (c TogglClient) RunTagCleaner() {
120
	time.Sleep(1 * time.Second) // wait for server start
121
	for {
122
		entry, err := c.Session.GetCurrentTimeEntry()
123
		if err != nil {
124
			c.Logger.Println("[ERROR] не удалось получить текущую toggl-запись")
125
			continue
126
		}
127
128
		// delete sent tag
129
		for _, tag := range entry.Tags {
130
			if tag == c.Config.TogglSentTag {
131
				c.Logger.Printf("[INFO] убран тег %s из текущей записи %s", c.Config.TogglSentTag, entry.Description)
132
				c.Session.AddRemoveTag(entry.ID, c.Config.TogglSentTag, false)
133
			}
134
		}
135
136
		time.Sleep(1 * time.Minute)
137
	}
138
}
139
140
// SendToPlanfix получает записи из Toggl и отправляет в Планфикс
141
// * нужна, чтобы сохранился c.PlanfixAPI.Sid при авторизации
142
func (c *TogglClient) SendToPlanfix() (err error) {
143
	c.Logger.Println("[INFO] отправка в Планфикс")
144
	pendingEntries, err := c.GetPendingEntries()
145
	if err != nil {
146
		return err
147
	}
148
	c.Logger.Printf("[INFO] в очереди на отправку: %d", len(pendingEntries))
149
	days := c.GroupEntriesByDay(pendingEntries)
150
	for day, dayEntries := range days {
151
		tasks := c.GroupEntriesByTask(dayEntries)
152
		minsTotal := 0
153
154
		for taskID, entries := range tasks {
155
			err, mins := c.sendEntries(taskID, entries)
156
157
			minsTotal += mins
158
			// add to day time
159
			if dayMins, ok := c.SentLog[day]; ok {
160
				c.SentLog[day] = dayMins + mins
161
			} else {
162
				c.SentLog[day] = mins
163
			}
164
165
			taskURL := fmt.Sprintf("https://%s.planfix.ru/task/%d", c.Config.PlanfixAccount, taskID)
166
			if err != nil {
167
				c.Logger.Printf("[ERROR] записи к задаче %s (%s) не удалось отправить: %s", taskURL, day, err)
168
			} else {
169
				c.Logger.Printf("[INFO] %d минут отправлены на %s (%s)", mins, taskURL, day)
170
			}
171
		}
172
		dayHours := int(c.SentLog[day] / 60)
173
		dayMins := c.SentLog[day] % 60
174
		dayTime := fmt.Sprintf("%dч%02dм", dayHours, dayMins)
175
		dayString := strings.ReplaceAll(day, "-", ".")
176
177
		msg := fmt.Sprintf("%s, %s, всего %s за %s", util.PluralMins(minsTotal), util.PluralTasks(len(tasks)), dayTime, dayString)
178
		if c.Config.DryRun {
179
			msg += " (тестовый режим)"
180
		}
181
		c.Logger.Printf("[INFO] %s", msg)
182
		util.Notify(msg)
183
	}
184
	c.Opts["LastSent"] = time.Now().Format("15:04:05")
185
	return nil
186
}
187
188
// GroupEntriesByDay объединяет плоский список toggl-записей в map c ключом - Y-m-d
189
func (c TogglClient) GroupEntriesByDay(entries []TogglPlanfixEntry) (grouped map[string][]TogglPlanfixEntry) {
190
	grouped = make(map[string][]TogglPlanfixEntry)
191
	if len(entries) == 0 {
192
		return grouped
193
	}
194
	for _, entry := range entries {
195
		day := entry.Start.Format("02-01-2006")
196
		grouped[day] = append(grouped[day], entry)
197
	}
198
	return grouped
199
}
200
201
// GroupEntriesByTask объединяет плоский список toggl-записей в map c ключом - ID задачи в Планфиксе
202
func (c TogglClient) GroupEntriesByTask(entries []TogglPlanfixEntry) (grouped map[int][]TogglPlanfixEntry) {
203
	grouped = make(map[int][]TogglPlanfixEntry)
204
	if len(entries) == 0 {
205
		return grouped
206
	}
207
	for _, entry := range entries {
208
		grouped[entry.Planfix.TaskID] = append(grouped[entry.Planfix.TaskID], entry)
209
	}
210
	return grouped
211
}
212
213
// SumEntriesGroup объединяет несколько toggl-записей в одну с просуммированным временем
214
// Входной map формируется через GroupEntriesByTask
215
// Ключ массива - ID задачи в Планфиксе
216
func (c TogglClient) SumEntriesGroup(grouped map[int][]TogglPlanfixEntry) (summed []TogglPlanfixEntry) {
217
	g := make(map[int]TogglPlanfixEntry)
218
	for taskID, entries := range grouped {
219
		for _, entry := range entries {
220
			if ge, ok := g[taskID]; ok {
221
				ge.Duration += entry.Duration
222
				ge.Planfix.GroupCount++
223
				g[entry.Planfix.TaskID] = ge
224
			} else {
225
				entry.Description = c.getSumEntryName(entries)
226
				g[entry.Planfix.TaskID] = entry
227
			}
228
		}
229
	}
230
231
	taskIDs := make([]int, 0, len(g))
232
	for k := range g {
233
		taskIDs = append(taskIDs, k)
234
	}
235
	sort.Ints(taskIDs)
236
237
	summed = make([]TogglPlanfixEntry, 0, len(g))
238
	for _, taskID := range taskIDs {
239
		summed = append(summed, g[taskID])
240
	}
241
242
	return summed
243
}
244
245
func (c TogglClient) getSumEntryName(entries []TogglPlanfixEntry) string {
246
	names := []string{}
247
	for _, entry := range entries {
248
		names = append(names, entry.Description)
249
	}
250
	// sort
251
	sort.Strings(names)
252
253
	// group
254
	groupNamesCounts := make(map[string]int)
255
	for _, name := range names {
256
		groupNamesCounts[name]++
257
	}
258
259
	// keys
260
	names = []string{}
261
	for name, _ := range groupNamesCounts {
262
		names = append(names, name)
263
	}
264
265
	return strings.Join(names, "\n")
266
}
267
268
// GetTogglUser возвращает юзера в Toggl
269
func (c TogglClient) GetTogglUser() (account toggl.Account, err error) {
270
	account, err = c.Session.GetAccount()
271
	if err != nil {
272
		return account, fmt.Errorf("GetTogglUser: Не удалось получить Toggl UserID, проверьте TogglAPIToken, %s", err.Error())
273
	}
274
	return account, nil
275
}
276
277
// IsWorkspaceExists проверяет наличие workspace в доступных
278
func (c TogglClient) IsWorkspaceExists(wid int) (bool, error) {
279
	ws, err := c.Session.GetWorkspaces()
280
	if err != nil {
281
		return false, fmt.Errorf("Не удалось получить Toggl workspaces, проверьте TogglAPIToken, %s", err.Error())
282
	}
283
	for _, w := range ws {
284
		if w.ID == wid {
285
			return true, nil
286
		}
287
	}
288
	return false, nil
289
}
290
291
// GetPlanfixUser возвращает юзера в Планфиксе
292
func (c TogglClient) GetPlanfixUser() (user planfix.XMLResponseUser, err error) {
293
	var userResponse planfix.XMLResponseUserGet
294
	userResponse, err = c.PlanfixAPI.UserGet(0)
295
	user = userResponse.User
296
	if err != nil {
297
		return user, fmt.Errorf("Не удалось получить Planfix UserID, проверьте PlanfixAPIKey, PlanfixAPIUrl, PlanfixUserName, PlanfixUserPassword, %s", err.Error())
298
	}
299
	return user, nil
300
}
301
302
func (c TogglClient) ReportToTogglPlanfixEntry(report toggl.DetailedReport) (entries []TogglPlanfixEntry) {
303
	for _, entry := range report.Data {
304
		pfe := c.togglDetailedEntryToPlanfixTogglEntry(entry)
305
		entries = append(entries, pfe)
306
	}
307
	return entries
308
}
309
310
func (c TogglClient) togglEntryToTogglDetailedEntry(entry toggl.TimeEntry) toggl.DetailedTimeEntry {
311
	return toggl.DetailedTimeEntry{
312
		ID:          entry.ID,
313
		Pid:         entry.Pid,
314
		Tid:         entry.Tid,
315
		Description: entry.Description,
316
		Start:       entry.Start,
317
		End:         entry.Stop,
318
		Tags:        entry.Tags,
319
		Duration:    entry.Duration,
320
		Billable:    entry.Billable,
321
	}
322
}
323
324
func (c TogglClient) togglDetailedEntryToPlanfixTogglEntry(entry toggl.DetailedTimeEntry) (pfe TogglPlanfixEntry) {
325
	pfe = TogglPlanfixEntry{
326
		DetailedTimeEntry: entry,
327
		Planfix: PlanfixEntryData{
328
			Sent:       false,
329
			TaskID:     0,
330
			GroupCount: 1,
331
		},
332
	}
333
334
	for _, tag := range entry.Tags {
335
		// only digit == planfix.task_id
336
		regex := regexp.MustCompile(`^\d+$`)
337
		if regex.MatchString(tag) {
338
			pfe.Planfix.TaskID, _ = strconv.Atoi(tag)
339
		}
340
341
		// sent tag
342
		if tag == c.Config.TogglSentTag {
343
			pfe.Planfix.Sent = true
344
		}
345
	}
346
347
	return pfe
348
}
349
350
// GetEntries получает []toggl.DetailedTimeEntry и превращает их в []TogglPlanfixEntry с подмешенными данными Планфикса
351
func (c TogglClient) GetCurrentEntry() (entry TogglPlanfixEntry, err error) {
352
	togglEntry, err := c.Session.GetCurrentTimeEntry()
353
	detailedEntry := c.togglEntryToTogglDetailedEntry(togglEntry)
354
	entry = c.togglDetailedEntryToPlanfixTogglEntry(detailedEntry)
355
	return entry, err
356
}
357
358
// GetEntries получает []toggl.DetailedTimeEntry и превращает их в []TogglPlanfixEntry с подмешенными данными Планфикса
359
func (c TogglClient) GetEntries(togglWorkspaceID int, since, until string) (entries []TogglPlanfixEntry, err error) {
360
	report, err := c.Session.GetDetailedReport(togglWorkspaceID, since, until, 1)
361
	if err != nil {
362
		c.Logger.Printf("[ERROR] Toggl: %s", err)
363
	}
364
365
	entries = c.ReportToTogglPlanfixEntry(report)
366
	return entries, nil
367
}
368
369
func (c TogglClient) GetReport(rp toggl.DetailedReportParams) toggl.DetailedReport {
370
	rp.WorkspaceID = c.Config.TogglWorkspaceID
371
	rp.Rounding = true
372
	report, err := c.Session.GetDetailedReportV2(rp)
373
	if err != nil {
374
		c.Logger.Printf("[ERROR] Toggl: %s", err)
375
	}
376
	return report
377
}
378
379
func (c TogglClient) GetReportV1(togglWorkspaceID int, since, until string, page int) toggl.DetailedReport {
380
	report, err := c.Session.GetDetailedReport(togglWorkspaceID, since, until, page)
381
	if err != nil {
382
		c.Logger.Printf("[ERROR] Toggl: %s", err)
383
	}
384
	return report
385
}
386
387
func (c TogglClient) getDetailedReportParams() {
388
	return
389
}
390
391
func (c TogglClient) GetEntriesV2(rp toggl.DetailedReportParams) (entries []TogglPlanfixEntry, err error) {
392
	report := c.GetReport(rp)
393
	entries = c.ReportToTogglPlanfixEntry(report)
394
	return entries, nil
395
}
396
397
func (c TogglClient) GetEntriesByTag(tagName string) (entries []TogglPlanfixEntry, err error) {
398
	tag, err := c.Session.GetTagByName(tagName, c.Config.TogglWorkspaceID)
399
	if err != nil {
400
		return entries, err
401
	}
402
	report := c.GetReport(toggl.DetailedReportParams{
403
		// TODO: defaultReportParams
404
		TagIDs: []int{tag.ID},
405
	})
406
	entries = c.ReportToTogglPlanfixEntry(report)
407
	return entries, nil
408
}
409
410
// filter - хэлпер, фильтрующий toggl-записи
411
func filter(input []TogglPlanfixEntry, f func(entry TogglPlanfixEntry) bool) (output []TogglPlanfixEntry) {
412
	for _, v := range input {
413
		if f(v) {
414
			output = append(output, v)
415
		}
416
	}
417
	return output
418
}
419
420
// GetPendingEntriesPage возвращает toggl-записи, которые должны быть отправлены в Планфикс для конкретной страницы
421
func (c TogglClient) getPendingEntriesPage(page int) (entries []TogglPlanfixEntry, err error) {
422
	entries, err = c.GetEntriesV2(toggl.DetailedReportParams{
423
		Page: page,
424
	})
425
	if err != nil {
426
		return []TogglPlanfixEntry{}, err
427
	}
428
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return entry.Planfix.TaskID != 0 })
429
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return !entry.Planfix.Sent })
430
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return entry.Uid == c.Config.TogglUserID })
431
	return entries, nil
432
}
433
434
// GetPendingEntries возвращает toggl-записи, которые должны быть отправлены в Планфикс
435
func (c TogglClient) GetPendingEntries() (entries []TogglPlanfixEntry, err error) {
436
	maxPages := 20
437
	for currentPage := 1; currentPage <= maxPages; currentPage++ {
438
		pageEntries, err := c.getPendingEntriesPage(currentPage)
439
		if err != nil {
440
			return entries, err
441
		}
442
		if len(pageEntries) == 0 {
443
			break
444
		}
445
		entries = append(entries, pageEntries...)
446
	}
447
	return entries, err
448
}
449
450
// sendEntries отправляет toggl-записи в Планфикс и помечает их в Toggl тегом sent
451
func (c TogglClient) sendEntries(planfixTaskID int, entries []TogglPlanfixEntry) (error, int) {
452
	// будет точно просуммировано в одну
453
	sumEntry := c.SumEntriesGroup(map[int][]TogglPlanfixEntry{
454
		planfixTaskID: entries,
455
	})[0]
456
457
	date := sumEntry.Start.Format("2006-01-02")
458
	mins := int(math.Floor(float64(sumEntry.Duration)/60000 + .5))
459
	entryString := fmt.Sprintf(
460
		"[%s] %s (%d)",
461
		sumEntry.Project,
462
		sumEntry.Description,
463
		mins,
464
	)
465
	comment := fmt.Sprintf("toggl: %s", entryString)
466
467
	if c.Config.Debug {
468
		c.Logger.Printf("[DEBUG] sending %s", entryString)
469
	}
470
	if c.Config.DryRun {
471
		c.Logger.Println("[DEBUG] dry-run")
472
		return nil, mins
473
	}
474
475
	// send to planfix
476
	var err error
477
	if c.Config.PlanfixUserID != 0 {
478
		err = c.sendWithPlanfixAPI(planfixTaskID, date, mins, comment)
479
	} else {
480
		err = c.sendWithSMTP(planfixTaskID, date, mins)
481
	}
482
	if err != nil {
483
		c.Logger.Printf("[ERROR] %v", err)
484
		return err, mins
485
	}
486
487
	return c.markAsSent(entries), mins
488
}
489
490
// markAsSent отмечает toggl-записи тегом sent
491
func (c TogglClient) markAsSent(entries []TogglPlanfixEntry) error {
492
	for _, entry := range entries {
493
		entryString := fmt.Sprintf(
494
			"[%s] %s (%d)",
495
			entry.Project,
496
			entry.Description,
497
			int(math.Floor(float64(entry.Duration)/60000+.5)),
498
		)
499
		if c.Config.Debug {
500
			c.Logger.Printf("[DEBUG] marking %s in toggl", entryString)
501
		}
502
		if _, err := c.Session.AddRemoveTag(entry.ID, c.Config.TogglSentTag, true); err != nil {
503
			c.Logger.Println(err)
504
			return err
505
		}
506
	}
507
	return nil
508
}
509
510
// sendWithSMTP отправляет toggl-запись через SMTP
511
func (c TogglClient) sendWithSMTP(planfixTaskID int, date string, mins int) error {
512
	auth := smtp.PlainAuth("", c.Config.SMTPLogin, c.Config.SMTPPassword, c.Config.SMTPHost)
513
	taskEmail := c.getTaskEmail(planfixTaskID)
514
	to := []string{taskEmail}
515
	if c.Config.Debug {
516
		testEmail := c.Config.SMTPEmailFrom
517
		to = append(to, testEmail)
518
	}
519
	body := c.getEmailBody(planfixTaskID, date, mins)
520
	msg := []byte(body)
521
	return smtp.SendMail(fmt.Sprintf("%s:%d", c.Config.SMTPHost, c.Config.SMTPPort), auth, c.Config.SMTPEmailFrom, to, msg)
522
}
523
524
// getTaskEmail возвращает email задачи по ее номеру
525
func (c TogglClient) getTaskEmail(planfixTaskID int) string {
526
	return fmt.Sprintf("task+%d@%s.planfix.ru", planfixTaskID, c.Config.PlanfixAccount)
527
}
528
529
// getEmailBody возвращает email body для отправки в Планфикс
530
func (c TogglClient) getEmailBody(planfixTaskID int, date string, mins int) string {
531
	taskEmail := c.getTaskEmail(planfixTaskID)
532
	return fmt.Sprintf(
533
		"Content-Type: text/plain; charset=\"utf-8\"\r\n"+
534
			"From: %s\r\n"+
535
			"To: %s\r\n"+
536
			"Subject: @toggl @nonotify\r\n"+
537
			"\r\n"+
538
			"Вид работы: %s\r\n"+
539
			"time: %d\r\n"+
540
			"Автор: %s\r\n"+
541
			"Дата: %s\r\n",
542
		c.Config.SMTPEmailFrom,
543
		taskEmail,
544
		c.Config.PlanfixAnaliticTypeValue,
545
		mins,
546
		c.Config.PlanfixAuthorName,
547
		date,
548
	)
549
}
550
551
// sendWithPlanfixAPI отправляет toggl-запись через Планфикс API
552
func (c TogglClient) sendWithPlanfixAPI(planfixTaskID int, date string, mins int, comment string) error {
553
	userIDs := struct {
554
		ID []int `xml:"id"`
555
	}{[]int{c.Config.PlanfixUserID}}
556
557
	_, err := c.PlanfixAPI.ActionAdd(planfix.XMLRequestActionAdd{
558
		TaskGeneral: planfixTaskID,
559
		Description: "",
560
		Analitics: []planfix.XMLRequestActionAnalitic{
561
			{
562
				ID: c.analiticData.ID,
563
				// аналитика должна содержать поля: вид работы, кол-во, дата, коммент, юзеры
564
				ItemData: []planfix.XMLRequestAnaliticField{
565
					{FieldID: c.analiticData.TypeID, Value: c.analiticData.TypeValueID}, // name
566
					{FieldID: c.analiticData.CountID, Value: mins},                      // count, минут
567
					{FieldID: c.analiticData.CommentID, Value: comment},                 // comment
568
					{FieldID: c.analiticData.DateID, Value: date},                       // date
569
					{FieldID: c.analiticData.UsersID, Value: userIDs},                   // user
570
				},
571
			},
572
		},
573
	})
574
	return err
575
}
576
577
// GetAnaliticDataCached получает аналитику из кеша (по возможности)
578
func (c *TogglClient) GetAnaliticDataCached(name, typeName, typeValue, countName, commentName, dateName, usersName string) (PlanfixAnaliticData, error) {
579
	if c.analiticData.ID != 0 { // only on first call
580
		return c.analiticData, nil
581
	}
582
	data, err := c.GetAnaliticData(name, typeName, typeValue, countName, commentName, dateName, usersName)
583
	c.analiticData = data
584
	return c.analiticData, err
585
}
586
587
// GetAnaliticData получает ID аналитики и ее полей из названий аналитики и полей
588
func (c *TogglClient) GetAnaliticData(name, typeName, typeValue, countName, commentName, dateName, usersName string) (PlanfixAnaliticData, error) {
589
	// получение аналитики
590
	analitic, err := c.PlanfixAPI.GetAnaliticByName(name)
591
	if err != nil {
592
		return PlanfixAnaliticData{}, err
593
	}
594
595
	// получение полей аналитики
596
	analiticOptions, err := c.PlanfixAPI.AnaliticGetOptions(analitic.ID)
597
	if err != nil {
598
		return PlanfixAnaliticData{}, err
599
	}
600
601
	analiticData := PlanfixAnaliticData{
602
		ID: analitic.ID,
603
	}
604
605
	// получение ID полей по их названиям
606
	for _, field := range analiticOptions.Analitic.Fields {
607
		switch field.Name {
608
		case typeName:
609
			analiticData.TypeID = field.ID
610
			// получение ID записи справочника
611
			record, _ := c.PlanfixAPI.GetHandbookRecordByName(field.HandbookID, typeValue)
612
			analiticData.TypeValueID = record.Key
613
		case countName:
614
			analiticData.CountID = field.ID
615
		case commentName:
616
			analiticData.CommentID = field.ID
617
		case dateName:
618
			analiticData.DateID = field.ID
619
		case usersName:
620
			analiticData.UsersID = field.ID
621
		}
622
	}
623
624
	err = c.isAnaliticValid(analiticData)
625
	return analiticData, err
626
}
627
628
// isAnaliticValid проходит по всем полям структуры PlanfixAnaliticData и возвращает ошибку, если хоть один ID == 0
629
func (c TogglClient) isAnaliticValid(data PlanfixAnaliticData) error {
630
	var errors []string
631
	v := reflect.ValueOf(data)
632
	typeOf := v.Type()
633
	//values := make([]interface{}, v.NumField())
634
	for i := 0; i < v.NumField(); i++ {
635
		name := typeOf.Field(i).Name
636
		value := v.Field(i).Int()
637
		if value == 0 {
638
			errors = append(errors, fmt.Sprintf("%s not found", name))
639
		}
640
	}
641
	if len(errors) != 0 {
642
		return fmt.Errorf(strings.Join(errors, ", "))
0 ignored issues
show
can't check non-constant format in call to Errorf
Loading history...
643
	}
644
	return nil
645
}
646
647
func (c TogglClient) Notify(msg string) error {
648
	err := beeep.Notify("", msg, "assets/icon.png")
649
	return err
650
}
651