Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f153d7f32 | ||
|
|
9c6a2dbf96 | ||
|
|
93c91161ba | ||
|
|
1620e12115 |
@@ -15,6 +15,7 @@
|
|||||||
- 💾 **Экспорт** - экспорт данных в JSON или CSV
|
- 💾 **Экспорт** - экспорт данных в JSON или CSV
|
||||||
- 🔊 **Звуковые уведомления** - настраиваемые звуковые оповещения
|
- 🔊 **Звуковые уведомления** - настраиваемые звуковые оповещения
|
||||||
- 🌐 **Локализация** - русский и английский интерфейс
|
- 🌐 **Локализация** - русский и английский интерфейс
|
||||||
|
- 🔗 **Jira интеграция** - автоматическое открытие Jira-фильтра в браузере при ходе участника
|
||||||
- 🔄 **Автообновление** - проверка и установка обновлений из приложения
|
- 🔄 **Автообновление** - проверка и установка обновлений из приложения
|
||||||
|
|
||||||
## Скриншоты
|
## Скриншоты
|
||||||
@@ -96,8 +97,9 @@ xattr -cr "Daily Timer.app"
|
|||||||
|
|
||||||
1. **Добавить участников** - ввести имена и установить лимиты времени
|
1. **Добавить участников** - ввести имена и установить лимиты времени
|
||||||
2. **Установить общий лимит** - настроить длительность митинга (по умолчанию: 60 минут)
|
2. **Установить общий лимит** - настроить длительность митинга (по умолчанию: 60 минут)
|
||||||
3. **Упорядочить** - перетащить для изменения порядка выступлений
|
3. **Настроить Jira URL** - указать базовый URL доски Jira (опционально), для каждого участника задать quickFilter ID
|
||||||
4. **Отметить присутствие** - переключить статус присутствия/отсутствия
|
4. **Упорядочить** - перетащить для изменения порядка выступлений
|
||||||
|
5. **Отметить присутствие** - переключить статус присутствия/отсутствия
|
||||||
|
|
||||||
### Во время митинга
|
### Во время митинга
|
||||||
|
|
||||||
@@ -186,7 +188,7 @@ GITEA_TOKEN=<token> make release-publish
|
|||||||
|
|
||||||
## Планы
|
## Планы
|
||||||
|
|
||||||
- [ ] Drag-and-drop для порядка участников
|
- [x] Drag-and-drop для порядка участников
|
||||||
- [ ] Интеграция с Telegram (отправка сводки митинга)
|
- [ ] Интеграция с Telegram (отправка сводки митинга)
|
||||||
- [ ] Интеграция с календарём (авто-расписание)
|
- [ ] Интеграция с календарём (авто-расписание)
|
||||||
- [ ] Шаблоны команд
|
- [ ] Шаблоны команд
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
let saving = false
|
let saving = false
|
||||||
let meetingLimitMin = 15
|
let meetingLimitMin = 15
|
||||||
let defaultTimeMin = 2
|
let defaultTimeMin = 2
|
||||||
|
let defaultJiraUrl = ''
|
||||||
|
let editingDefaultJiraUrl = false
|
||||||
|
let defaultJiraUrlInput = ''
|
||||||
let windowWidth = 800
|
let windowWidth = 800
|
||||||
let windowFullHeight = true
|
let windowFullHeight = true
|
||||||
let audioContext = null
|
let audioContext = null
|
||||||
@@ -236,6 +239,7 @@
|
|||||||
}
|
}
|
||||||
if (settings) {
|
if (settings) {
|
||||||
defaultTimeMin = Math.floor(settings.defaultParticipantTime / 60)
|
defaultTimeMin = Math.floor(settings.defaultParticipantTime / 60)
|
||||||
|
defaultJiraUrl = settings.defaultJiraUrl || 'https://jira.ncloudtech.ru/secure/RapidBoard.jspa?rapidView=1431&projectKey=RNDIN'
|
||||||
windowWidth = settings.windowWidth || 800
|
windowWidth = settings.windowWidth || 800
|
||||||
windowFullHeight = settings.windowFullHeight !== false
|
windowFullHeight = settings.windowFullHeight !== false
|
||||||
}
|
}
|
||||||
@@ -251,6 +255,7 @@
|
|||||||
saving = true
|
saving = true
|
||||||
try {
|
try {
|
||||||
settings.defaultParticipantTime = defaultTimeMin * 60
|
settings.defaultParticipantTime = defaultTimeMin * 60
|
||||||
|
settings.defaultJiraUrl = defaultJiraUrl.trim()
|
||||||
meeting.timeLimit = meetingLimitMin * 60
|
meeting.timeLimit = meetingLimitMin * 60
|
||||||
settings.windowWidth = Math.max(480, windowWidth)
|
settings.windowWidth = Math.max(480, windowWidth)
|
||||||
settings.windowFullHeight = windowFullHeight
|
settings.windowFullHeight = windowFullHeight
|
||||||
@@ -312,6 +317,29 @@
|
|||||||
<label for="meetingLimit">{$t('settings.defaultTotalTime')}</label>
|
<label for="meetingLimit">{$t('settings.defaultTotalTime')}</label>
|
||||||
<input type="number" id="meetingLimit" bind:value={meetingLimitMin} min="1" max="120" />
|
<input type="number" id="meetingLimit" bind:value={meetingLimitMin} min="1" max="120" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="defaultJiraUrl">{$t('settings.defaultJiraUrl')}</label>
|
||||||
|
{#if editingDefaultJiraUrl}
|
||||||
|
<div class="jira-edit-inline">
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input type="url" bind:value={defaultJiraUrlInput} autofocus
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') { defaultJiraUrl = defaultJiraUrlInput; editingDefaultJiraUrl = false }
|
||||||
|
if (e.key === 'Escape') editingDefaultJiraUrl = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button class="jira-inline-save" on:click={() => { defaultJiraUrl = defaultJiraUrlInput; editingDefaultJiraUrl = false }}>✓</button>
|
||||||
|
<button class="jira-inline-cancel" on:click={() => editingDefaultJiraUrl = false}>✗</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||||
|
<div class="jira-display" on:click={() => { defaultJiraUrlInput = defaultJiraUrl; editingDefaultJiraUrl = true }}>
|
||||||
|
<span class="jira-url-text">{defaultJiraUrl || '—'}</span>
|
||||||
|
<span class="jira-edit-icon">✎</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -526,7 +554,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"] {
|
input[type="number"],
|
||||||
|
input[type="url"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid #3d4f61;
|
border: 1px solid #3d4f61;
|
||||||
@@ -830,4 +859,79 @@
|
|||||||
background: #3b7dc9;
|
background: #3b7dc9;
|
||||||
border-color: #3b7dc9;
|
border-color: #3b7dc9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jira-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #3d4f61;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1b2636;
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-display:hover {
|
||||||
|
border-color: #4a90d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-url-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-edit-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-display:hover .jira-edit-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-edit-inline {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-edit-inline input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #4a90d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1b2636;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-inline-save,
|
||||||
|
.jira-inline-cancel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 42px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-inline-save {
|
||||||
|
background: #22c55e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-inline-cancel {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, createEventDispatcher } from 'svelte'
|
import { onMount, createEventDispatcher } from 'svelte'
|
||||||
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting } from '../../wailsjs/go/app/App'
|
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting, GetSettings } from '../../wailsjs/go/app/App'
|
||||||
import { t } from '../lib/i18n'
|
import { t } from '../lib/i18n'
|
||||||
import { attendance } from '../lib/stores'
|
import { attendance } from '../lib/stores'
|
||||||
|
|
||||||
@@ -17,15 +17,55 @@
|
|||||||
let editingId = null
|
let editingId = null
|
||||||
let editName = ''
|
let editName = ''
|
||||||
let editTimeLimitMin = 2
|
let editTimeLimitMin = 2
|
||||||
|
let editJiraFilter = ''
|
||||||
|
|
||||||
|
// Quick jira filter edit
|
||||||
|
let quickFilterEditId = null
|
||||||
|
let quickFilterInput = ''
|
||||||
|
|
||||||
// Meeting name editing
|
// Meeting name editing
|
||||||
let editingMeetingName = false
|
let editingMeetingName = false
|
||||||
let meetingNameInput = ''
|
let meetingNameInput = ''
|
||||||
|
let originalMeetingName = ''
|
||||||
|
|
||||||
// Meeting time editing
|
// Meeting time editing
|
||||||
let editingMeetingTime = false
|
let editingMeetingTime = false
|
||||||
let meetingTimeInput = 60
|
let meetingTimeInput = 60
|
||||||
|
let originalMeetingTime = 60
|
||||||
|
|
||||||
|
// Meeting Jira URL editing
|
||||||
|
let editingMeetingJiraUrl = false
|
||||||
|
let meetingJiraUrlInput = ''
|
||||||
|
let originalMeetingJiraUrl = ''
|
||||||
|
let defaultJiraUrl = ''
|
||||||
|
|
||||||
|
// Confirmation dialog when switching between editable fields
|
||||||
|
let confirmDialog = null
|
||||||
|
|
||||||
|
function isDirty() {
|
||||||
|
if (editingMeetingName) return meetingNameInput !== originalMeetingName
|
||||||
|
if (editingMeetingTime) return meetingTimeInput !== originalMeetingTime
|
||||||
|
if (editingMeetingJiraUrl) return meetingJiraUrlInput !== originalMeetingJiraUrl
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveEdit() {
|
||||||
|
if (editingMeetingName) return { save: saveMeetingName, cancel: cancelEditMeetingName }
|
||||||
|
if (editingMeetingTime) return { save: saveMeetingTime, cancel: cancelEditMeetingTime }
|
||||||
|
if (editingMeetingJiraUrl) return { save: saveMeetingJiraUrl, cancel: cancelEditMeetingJiraUrl }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function guardEdit(startFn) {
|
||||||
|
const active = getActiveEdit()
|
||||||
|
if (!active) { startFn(); return }
|
||||||
|
if (!isDirty()) { active.cancel(); startFn(); return }
|
||||||
|
confirmDialog = {
|
||||||
|
save: async () => { confirmDialog = null; await active.save(); startFn() },
|
||||||
|
discard: () => { confirmDialog = null; active.cancel(); startFn() },
|
||||||
|
cancel: () => { confirmDialog = null }
|
||||||
|
}
|
||||||
|
}
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadData()
|
await loadData()
|
||||||
})
|
})
|
||||||
@@ -35,6 +75,8 @@
|
|||||||
try {
|
try {
|
||||||
participants = await GetParticipants()
|
participants = await GetParticipants()
|
||||||
meeting = await GetMeeting()
|
meeting = await GetMeeting()
|
||||||
|
const appSettings = await GetSettings()
|
||||||
|
defaultJiraUrl = appSettings?.defaultJiraUrl || 'https://jira.ncloudtech.ru/secure/RapidBoard.jspa?rapidView=1431&projectKey=RNDIN'
|
||||||
|
|
||||||
selectedOrder = participants.map(p => p.id)
|
selectedOrder = participants.map(p => p.id)
|
||||||
attendance.init(participants)
|
attendance.init(participants)
|
||||||
@@ -48,7 +90,7 @@
|
|||||||
if (!newName.trim()) return
|
if (!newName.trim()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AddParticipant(newName.trim(), '', newTimeLimitMin * 60)
|
await AddParticipant(newName.trim(), '', newTimeLimitMin * 60, '')
|
||||||
newName = ''
|
newName = ''
|
||||||
await loadData()
|
await loadData()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -71,19 +113,43 @@
|
|||||||
editingId = p.id
|
editingId = p.id
|
||||||
editName = p.name
|
editName = p.name
|
||||||
editTimeLimitMin = Math.floor(p.timeLimit / 60)
|
editTimeLimitMin = Math.floor(p.timeLimit / 60)
|
||||||
|
editJiraFilter = p.jiraFilter || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function startQuickFilterEdit(p) {
|
||||||
|
quickFilterEditId = p.id
|
||||||
|
quickFilterInput = p.jiraFilter || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelQuickFilterEdit() {
|
||||||
|
quickFilterEditId = null
|
||||||
|
quickFilterInput = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveQuickFilterEdit() {
|
||||||
|
const p = participants.find(x => x.id === quickFilterEditId)
|
||||||
|
if (!p) return
|
||||||
|
try {
|
||||||
|
await UpdateParticipant(p.id, p.name, '', p.timeLimit, quickFilterInput.trim())
|
||||||
|
quickFilterEditId = null
|
||||||
|
await loadData()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to update jira filter:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingId = null
|
editingId = null
|
||||||
editName = ''
|
editName = ''
|
||||||
editTimeLimitMin = 2
|
editTimeLimitMin = 2
|
||||||
|
editJiraFilter = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEdit() {
|
async function saveEdit() {
|
||||||
if (!editName.trim() || editingId === null) return
|
if (!editName.trim() || editingId === null) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await UpdateParticipant(editingId, editName.trim(), '', editTimeLimitMin * 60)
|
await UpdateParticipant(editingId, editName.trim(), '', editTimeLimitMin * 60, editJiraFilter.trim())
|
||||||
editingId = null
|
editingId = null
|
||||||
await loadData()
|
await loadData()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -175,7 +241,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startEditMeetingName() {
|
function startEditMeetingName() {
|
||||||
meetingNameInput = meeting?.name || ''
|
originalMeetingName = meeting?.name || ''
|
||||||
|
meetingNameInput = originalMeetingName
|
||||||
editingMeetingName = true
|
editingMeetingName = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +254,7 @@
|
|||||||
async function saveMeetingName() {
|
async function saveMeetingName() {
|
||||||
if (!meetingNameInput.trim()) return
|
if (!meetingNameInput.trim()) return
|
||||||
try {
|
try {
|
||||||
await UpdateMeeting(meetingNameInput.trim(), meeting?.timeLimit || 3600)
|
await UpdateMeeting(meetingNameInput.trim(), meeting?.timeLimit || 3600, meeting?.jiraUrl || '')
|
||||||
meeting = await GetMeeting()
|
meeting = await GetMeeting()
|
||||||
editingMeetingName = false
|
editingMeetingName = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -196,7 +263,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startEditMeetingTime() {
|
function startEditMeetingTime() {
|
||||||
meetingTimeInput = Math.floor((meeting?.timeLimit || 3600) / 60)
|
originalMeetingTime = Math.floor((meeting?.timeLimit || 3600) / 60)
|
||||||
|
meetingTimeInput = originalMeetingTime
|
||||||
editingMeetingTime = true
|
editingMeetingTime = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +275,7 @@
|
|||||||
async function saveMeetingTime() {
|
async function saveMeetingTime() {
|
||||||
if (meetingTimeInput < 1) return
|
if (meetingTimeInput < 1) return
|
||||||
try {
|
try {
|
||||||
await UpdateMeeting(meeting?.name || 'Daily Standup', meetingTimeInput * 60)
|
await UpdateMeeting(meeting?.name || 'Daily Standup', meetingTimeInput * 60, meeting?.jiraUrl || '')
|
||||||
meeting = await GetMeeting()
|
meeting = await GetMeeting()
|
||||||
editingMeetingTime = false
|
editingMeetingTime = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -215,11 +283,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_JIRA_URL = defaultJiraUrl
|
||||||
|
|
||||||
|
function startEditMeetingJiraUrl() {
|
||||||
|
originalMeetingJiraUrl = meeting?.jiraUrl || defaultJiraUrl
|
||||||
|
meetingJiraUrlInput = originalMeetingJiraUrl
|
||||||
|
editingMeetingJiraUrl = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditMeetingJiraUrl() {
|
||||||
|
editingMeetingJiraUrl = false
|
||||||
|
meetingJiraUrlInput = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMeetingJiraUrl() {
|
||||||
|
try {
|
||||||
|
await UpdateMeeting(meeting?.name || 'Daily Standup', meeting?.timeLimit || 3600, meetingJiraUrlInput.trim())
|
||||||
|
meeting = await GetMeeting()
|
||||||
|
editingMeetingJiraUrl = false
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to update meeting jira url:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleGlobalKeydown(e) {
|
function handleGlobalKeydown(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
if (editingId !== null) cancelEdit()
|
if (editingId !== null) cancelEdit()
|
||||||
if (editingMeetingName) cancelEditMeetingName()
|
if (editingMeetingName) cancelEditMeetingName()
|
||||||
if (editingMeetingTime) cancelEditMeetingTime()
|
if (editingMeetingTime) cancelEditMeetingTime()
|
||||||
|
if (editingMeetingJiraUrl) cancelEditMeetingJiraUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -245,11 +337,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||||
<h1 on:click={startEditMeetingName} class="editable-title">
|
<h1 on:click={() => guardEdit(startEditMeetingName)} class="editable-title">
|
||||||
{meeting?.name || 'Daily Standup'}
|
{meeting?.name || 'Daily Standup'}
|
||||||
<span class="edit-icon">✎</span>
|
<span class="edit-icon">✎</span>
|
||||||
</h1>
|
</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="meeting-info-block">
|
||||||
{#if editingMeetingTime}
|
{#if editingMeetingTime}
|
||||||
<div class="meeting-time-edit">
|
<div class="meeting-time-edit">
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
@@ -270,13 +363,51 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||||
<p on:click={startEditMeetingTime} class="editable-time">
|
<p on:click={() => guardEdit(startEditMeetingTime)} class="editable-time">
|
||||||
{$t('setup.totalTime')}: {formatTime(meeting?.timeLimit || 900)}
|
{$t('setup.totalTime')}: {formatTime(meeting?.timeLimit || 900)}
|
||||||
<span class="edit-icon">✎</span>
|
<span class="edit-icon">✎</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if editingMeetingJiraUrl}
|
||||||
|
<div class="meeting-jira-edit">
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
bind:value={meetingJiraUrlInput}
|
||||||
|
placeholder={$t('setup.jiraUrlPlaceholder')}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') saveMeetingJiraUrl()
|
||||||
|
if (e.key === 'Escape') cancelEditMeetingJiraUrl()
|
||||||
|
}}
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button class="save-btn" on:click={saveMeetingJiraUrl}>✓</button>
|
||||||
|
<button class="cancel-btn" on:click={cancelEditMeetingJiraUrl}>✗</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||||
|
<p on:click={() => guardEdit(startEditMeetingJiraUrl)} class="editable-jira">
|
||||||
|
{$t('setup.jiraUrl')}: {#if meeting?.jiraUrl}<span class="jira-url-value">{meeting.jiraUrl}</span>{:else}<span class="jira-default-label">(Default)</span>{/if}
|
||||||
|
<span class="edit-icon">✎</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if confirmDialog}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||||
|
<div class="confirm-overlay" on:click={confirmDialog.cancel}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||||
|
<div class="confirm-dialog" on:click|stopPropagation>
|
||||||
|
<p class="confirm-text">Сохранить изменения?</p>
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<button class="confirm-discard" on:click={confirmDialog.discard}>Отменить</button>
|
||||||
|
<button class="confirm-save" on:click={confirmDialog.save}>Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="add-participant">
|
<div class="add-participant">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -334,6 +465,15 @@
|
|||||||
|
|
||||||
<span class="name">{p.name}</span>
|
<span class="name">{p.name}</span>
|
||||||
<span class="time-limit">{Math.floor(p.timeLimit / 60)} {$t('setup.minutes')}</span>
|
<span class="time-limit">{Math.floor(p.timeLimit / 60)} {$t('setup.minutes')}</span>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="url-indicator"
|
||||||
|
class:url-indicator--empty={!p.jiraFilter}
|
||||||
|
title={p.jiraFilter ? `${meeting?.jiraUrl}&quickFilter=${p.jiraFilter}` : $t('participants.jiraFilterEmpty')}
|
||||||
|
on:click|stopPropagation={() => startQuickFilterEdit(p)}
|
||||||
|
>🔗</span>
|
||||||
|
|
||||||
<button class="edit" on:click={() => startEdit(p)} title="{$t('participants.edit')}">✎</button>
|
<button class="edit" on:click={() => startEdit(p)} title="{$t('participants.edit')}">✎</button>
|
||||||
<button class="remove" on:click={() => handleRemove(id)}>×</button>
|
<button class="remove" on:click={() => handleRemove(id)}>×</button>
|
||||||
@@ -363,6 +503,13 @@
|
|||||||
if (e.key === 'Escape') cancelEdit()
|
if (e.key === 'Escape') cancelEdit()
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label for="editJiraFilter">{$t('participants.jiraFilter')}</label>
|
||||||
|
<input id="editJiraFilter" type="text" bind:value={editJiraFilter} placeholder={$t('participants.jiraFilterPlaceholder')} on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') saveEdit()
|
||||||
|
if (e.key === 'Escape') cancelEdit()
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button class="cancel-btn" on:click={cancelEdit}>{$t('common.cancel')}</button>
|
<button class="cancel-btn" on:click={cancelEdit}>{$t('common.cancel')}</button>
|
||||||
<button class="save-btn" on:click={saveEdit}>{$t('common.save')}</button>
|
<button class="save-btn" on:click={saveEdit}>{$t('common.save')}</button>
|
||||||
@@ -371,6 +518,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if quickFilterEditId !== null}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||||
|
<div class="edit-modal-overlay" on:click={cancelQuickFilterEdit}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||||
|
<div class="edit-modal quick-filter-modal" on:click|stopPropagation>
|
||||||
|
<h3>quickFilter ID</h3>
|
||||||
|
<div class="edit-field">
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input id="quickFilterInput" type="text" bind:value={quickFilterInput}
|
||||||
|
placeholder={$t('participants.jiraFilterPlaceholder')}
|
||||||
|
autofocus
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') saveQuickFilterEdit()
|
||||||
|
if (e.key === 'Escape') cancelQuickFilterEdit()
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button class="cancel-btn" on:click={cancelQuickFilterEdit}>{$t('common.cancel')}</button>
|
||||||
|
<button class="save-btn" on:click={saveQuickFilterEdit}>{$t('common.save')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<span>{$t('setup.participants')}: {Object.values($attendance).filter(Boolean).length} / {participants.length}</span>
|
<span>{$t('setup.participants')}: {Object.values($attendance).filter(Boolean).length} / {participants.length}</span>
|
||||||
<span>≈ {formatTime(selectedOrder.filter(id => $attendance[id]).reduce((acc, id) => acc + (getParticipant(id)?.timeLimit || 0), 0))}</span>
|
<span>≈ {formatTime(selectedOrder.filter(id => $attendance[id]).reduce((acc, id) => acc + (getParticipant(id)?.timeLimit || 0), 0))}</span>
|
||||||
@@ -390,11 +561,12 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -466,9 +638,11 @@
|
|||||||
|
|
||||||
.header p.editable-time {
|
.header p.editable-time {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header p.editable-time:hover {
|
.header p.editable-time:hover {
|
||||||
@@ -529,6 +703,89 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header p.editable-jira {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p.editable-jira:hover {
|
||||||
|
color: #4a90d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p.editable-jira:hover .edit-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-info-block {
|
||||||
|
margin: 12px 0 16px;
|
||||||
|
border: 1px solid #3d4f61;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-url-value {
|
||||||
|
max-width: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-default-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4b5563;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-jira-edit {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-jira-edit input {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 1px solid #4a90d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1b2636;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 280px;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-jira-edit .save-btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #166534;
|
||||||
|
color: #4ade80;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meeting-jira-edit .cancel-btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #991b1b;
|
||||||
|
color: #fca5a5;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.add-participant {
|
.add-participant {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -677,6 +934,28 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.url-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
filter: sepia(1) saturate(3) hue-rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-indicator:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-indicator--empty {
|
||||||
|
opacity: 0.2;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-indicator--empty:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.edit {
|
.edit {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -717,6 +996,61 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 120px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
background: #1e2d3d;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
min-width: 240px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-text {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-discard {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #374151;
|
||||||
|
color: #9ca3af;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-save {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #166534;
|
||||||
|
color: #4ade80;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-modal {
|
.edit-modal {
|
||||||
background: #232f3e;
|
background: #232f3e;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -725,6 +1059,10 @@
|
|||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-modal.quick-filter-modal {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-modal h3 {
|
.edit-modal h3 {
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export const translations = {
|
|||||||
deselectAll: 'Снять выбор',
|
deselectAll: 'Снять выбор',
|
||||||
startMeeting: 'Начать собрание',
|
startMeeting: 'Начать собрание',
|
||||||
speakerTime: 'Время на спикера',
|
speakerTime: 'Время на спикера',
|
||||||
totalTime: 'Общее время',
|
totalTime: 'Общее время собрания',
|
||||||
minutes: 'мин',
|
minutes: 'мин',
|
||||||
unlimited: 'Без ограничения',
|
unlimited: 'Без ограничения',
|
||||||
dragHint: 'перетащите для изменения порядка, ✓/✗ присутствие',
|
dragHint: 'перетащите для изменения порядка, ✓/✗ присутствие',
|
||||||
|
jiraUrl: 'Jira Kanban URL',
|
||||||
|
jiraUrlPlaceholder: 'https://jira.ncloudtech.ru/...',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Timer page
|
// Timer page
|
||||||
@@ -103,6 +105,7 @@ export const translations = {
|
|||||||
seconds: 'сек',
|
seconds: 'сек',
|
||||||
defaultSpeakerTime: 'Время на спикера по умолчанию',
|
defaultSpeakerTime: 'Время на спикера по умолчанию',
|
||||||
defaultTotalTime: 'Общее время собрания (мин)',
|
defaultTotalTime: 'Общее время собрания (мин)',
|
||||||
|
defaultJiraUrl: 'Jira URL Панели Kanban',
|
||||||
theme: 'Тема оформления',
|
theme: 'Тема оформления',
|
||||||
themeDark: 'Тёмная',
|
themeDark: 'Тёмная',
|
||||||
themeLight: 'Светлая',
|
themeLight: 'Светлая',
|
||||||
@@ -139,6 +142,9 @@ export const translations = {
|
|||||||
edit: 'Редактировать',
|
edit: 'Редактировать',
|
||||||
delete: 'Удалить',
|
delete: 'Удалить',
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
|
jiraFilter: 'Jira Kanban URL',
|
||||||
|
jiraFilterPlaceholder: 'quickFilter ID (напр. 12345)',
|
||||||
|
jiraFilterEmpty: 'Jira фильтр не задан — нажмите для настройки',
|
||||||
stats: 'Статистика',
|
stats: 'Статистика',
|
||||||
avgSpeakTime: 'Среднее время выступления',
|
avgSpeakTime: 'Среднее время выступления',
|
||||||
totalMeetings: 'Всего собраний',
|
totalMeetings: 'Всего собраний',
|
||||||
@@ -196,10 +202,12 @@ export const translations = {
|
|||||||
deselectAll: 'Deselect All',
|
deselectAll: 'Deselect All',
|
||||||
startMeeting: 'Start Meeting',
|
startMeeting: 'Start Meeting',
|
||||||
speakerTime: 'Speaker Time',
|
speakerTime: 'Speaker Time',
|
||||||
totalTime: 'Total Time',
|
totalTime: 'Total Meeting Time',
|
||||||
minutes: 'min',
|
minutes: 'min',
|
||||||
unlimited: 'Unlimited',
|
unlimited: 'Unlimited',
|
||||||
dragHint: 'drag to reorder, ✓/✗ attendance',
|
dragHint: 'drag to reorder, ✓/✗ attendance',
|
||||||
|
jiraUrl: 'Jira Kanban URL',
|
||||||
|
jiraUrlPlaceholder: 'https://jira.ncloudtech.ru/...',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Timer page
|
// Timer page
|
||||||
@@ -276,6 +284,7 @@ export const translations = {
|
|||||||
seconds: 'sec',
|
seconds: 'sec',
|
||||||
defaultSpeakerTime: 'Default Speaker Time',
|
defaultSpeakerTime: 'Default Speaker Time',
|
||||||
defaultTotalTime: 'Total meeting time (min)',
|
defaultTotalTime: 'Total meeting time (min)',
|
||||||
|
defaultJiraUrl: 'Jira Kanban Board URL',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
themeDark: 'Dark',
|
themeDark: 'Dark',
|
||||||
themeLight: 'Light',
|
themeLight: 'Light',
|
||||||
@@ -312,6 +321,9 @@ export const translations = {
|
|||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
|
jiraFilter: 'Jira Kanban URL',
|
||||||
|
jiraFilterPlaceholder: 'quickFilter ID (e.g. 12345)',
|
||||||
|
jiraFilterEmpty: 'Jira filter not set — click to configure',
|
||||||
stats: 'Statistics',
|
stats: 'Statistics',
|
||||||
avgSpeakTime: 'Avg Speaking Time',
|
avgSpeakTime: 'Avg Speaking Time',
|
||||||
totalMeetings: 'Total Meetings',
|
totalMeetings: 'Total Meetings',
|
||||||
|
|||||||
6
frontend/wailsjs/go/app/App.d.ts
vendored
6
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -3,7 +3,7 @@
|
|||||||
import {models} from '../models';
|
import {models} from '../models';
|
||||||
import {updater} from '../models';
|
import {updater} from '../models';
|
||||||
|
|
||||||
export function AddParticipant(arg1:string,arg2:string,arg3:number):Promise<models.Participant>;
|
export function AddParticipant(arg1:string,arg2:string,arg3:number,arg4:string):Promise<models.Participant>;
|
||||||
|
|
||||||
export function CheckForUpdates():Promise<updater.UpdateInfo>;
|
export function CheckForUpdates():Promise<updater.UpdateInfo>;
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ export function StopMeeting():Promise<void>;
|
|||||||
|
|
||||||
export function SwitchToSpeaker(arg1:number):Promise<void>;
|
export function SwitchToSpeaker(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function UpdateMeeting(arg1:string,arg2:number):Promise<void>;
|
export function UpdateMeeting(arg1:string,arg2:number,arg3:string):Promise<void>;
|
||||||
|
|
||||||
export function UpdateParticipant(arg1:number,arg2:string,arg3:string,arg4:number):Promise<void>;
|
export function UpdateParticipant(arg1:number,arg2:string,arg3:string,arg4:number,arg5:string):Promise<void>;
|
||||||
|
|
||||||
export function UpdateSettings(arg1:models.Settings):Promise<void>;
|
export function UpdateSettings(arg1:models.Settings):Promise<void>;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
export function AddParticipant(arg1, arg2, arg3) {
|
export function AddParticipant(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['app']['App']['AddParticipant'](arg1, arg2, arg3);
|
return window['go']['app']['App']['AddParticipant'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckForUpdates() {
|
export function CheckForUpdates() {
|
||||||
@@ -126,12 +126,12 @@ export function SwitchToSpeaker(arg1) {
|
|||||||
return window['go']['app']['App']['SwitchToSpeaker'](arg1);
|
return window['go']['app']['App']['SwitchToSpeaker'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UpdateMeeting(arg1, arg2) {
|
export function UpdateMeeting(arg1, arg2, arg3) {
|
||||||
return window['go']['app']['App']['UpdateMeeting'](arg1, arg2);
|
return window['go']['app']['App']['UpdateMeeting'](arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UpdateParticipant(arg1, arg2, arg3, arg4) {
|
export function UpdateParticipant(arg1, arg2, arg3, arg4, arg5) {
|
||||||
return window['go']['app']['App']['UpdateParticipant'](arg1, arg2, arg3, arg4);
|
return window['go']['app']['App']['UpdateParticipant'](arg1, arg2, arg3, arg4, arg5);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UpdateSettings(arg1) {
|
export function UpdateSettings(arg1) {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export namespace models {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
jiraFilter?: string;
|
||||||
timeLimit: number;
|
timeLimit: number;
|
||||||
order: number;
|
order: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@@ -129,6 +130,7 @@ export namespace models {
|
|||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
this.email = source["email"];
|
this.email = source["email"];
|
||||||
|
this.jiraFilter = source["jiraFilter"];
|
||||||
this.timeLimit = source["timeLimit"];
|
this.timeLimit = source["timeLimit"];
|
||||||
this.order = source["order"];
|
this.order = source["order"];
|
||||||
this.active = source["active"];
|
this.active = source["active"];
|
||||||
@@ -253,6 +255,7 @@ export namespace models {
|
|||||||
export class Meeting {
|
export class Meeting {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
jiraUrl?: string;
|
||||||
timeLimit: number;
|
timeLimit: number;
|
||||||
sessions?: MeetingSession[];
|
sessions?: MeetingSession[];
|
||||||
// Go type: time
|
// Go type: time
|
||||||
@@ -268,6 +271,7 @@ export namespace models {
|
|||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
|
this.jiraUrl = source["jiraUrl"];
|
||||||
this.timeLimit = source["timeLimit"];
|
this.timeLimit = source["timeLimit"];
|
||||||
this.sessions = this.convertValues(source["sessions"], MeetingSession);
|
this.sessions = this.convertValues(source["sessions"], MeetingSession);
|
||||||
this.createdAt = this.convertValues(source["createdAt"], null);
|
this.createdAt = this.convertValues(source["createdAt"], null);
|
||||||
@@ -329,6 +333,7 @@ export namespace models {
|
|||||||
windowFullHeight: boolean;
|
windowFullHeight: boolean;
|
||||||
windowX: number;
|
windowX: number;
|
||||||
windowY: number;
|
windowY: number;
|
||||||
|
defaultJiraUrl: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Settings(source);
|
return new Settings(source);
|
||||||
@@ -349,6 +354,7 @@ export namespace models {
|
|||||||
this.windowFullHeight = source["windowFullHeight"];
|
this.windowFullHeight = source["windowFullHeight"];
|
||||||
this.windowX = source["windowX"];
|
this.windowX = source["windowX"];
|
||||||
this.windowY = source["windowY"];
|
this.windowY = source["windowY"];
|
||||||
|
this.defaultJiraUrl = source["defaultJiraUrl"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class SpeakerInfo {
|
export class SpeakerInfo {
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
timer *timer.Timer
|
timer *timer.Timer
|
||||||
session *models.MeetingSession
|
session *models.MeetingSession
|
||||||
currentLogs map[uint]*models.ParticipantLog
|
currentLogs map[uint]*models.ParticipantLog
|
||||||
updater *updater.Updater
|
participantURLs map[uint]string
|
||||||
|
updater *updater.Updater
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(store *storage.Storage) *App {
|
func New(store *storage.Storage) *App {
|
||||||
@@ -85,16 +86,17 @@ func (a *App) GetParticipants() ([]models.Participant, error) {
|
|||||||
return a.store.GetParticipants()
|
return a.store.GetParticipants()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AddParticipant(name string, email string, timeLimit int) (*models.Participant, error) {
|
func (a *App) AddParticipant(name string, email string, timeLimit int, jiraFilter string) (*models.Participant, error) {
|
||||||
participants, _ := a.store.GetAllParticipants()
|
participants, _ := a.store.GetAllParticipants()
|
||||||
order := len(participants)
|
order := len(participants)
|
||||||
|
|
||||||
p := &models.Participant{
|
p := &models.Participant{
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: email,
|
Email: email,
|
||||||
TimeLimit: timeLimit,
|
JiraFilter: jiraFilter,
|
||||||
Order: order,
|
TimeLimit: timeLimit,
|
||||||
Active: true,
|
Order: order,
|
||||||
|
Active: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.store.CreateParticipant(p); err != nil {
|
if err := a.store.CreateParticipant(p); err != nil {
|
||||||
@@ -103,12 +105,13 @@ func (a *App) AddParticipant(name string, email string, timeLimit int) (*models.
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) UpdateParticipant(id uint, name string, email string, timeLimit int) error {
|
func (a *App) UpdateParticipant(id uint, name string, email string, timeLimit int, jiraFilter string) error {
|
||||||
p := &models.Participant{
|
p := &models.Participant{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: email,
|
Email: email,
|
||||||
TimeLimit: timeLimit,
|
JiraFilter: jiraFilter,
|
||||||
|
TimeLimit: timeLimit,
|
||||||
}
|
}
|
||||||
return a.store.UpdateParticipant(p)
|
return a.store.UpdateParticipant(p)
|
||||||
}
|
}
|
||||||
@@ -127,13 +130,14 @@ func (a *App) GetMeeting() (*models.Meeting, error) {
|
|||||||
return a.store.GetMeeting()
|
return a.store.GetMeeting()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) UpdateMeeting(name string, timeLimit int) error {
|
func (a *App) UpdateMeeting(name string, timeLimit int, jiraURL string) error {
|
||||||
meeting, err := a.store.GetMeeting()
|
meeting, err := a.store.GetMeeting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
meeting.Name = name
|
meeting.Name = name
|
||||||
meeting.TimeLimit = timeLimit
|
meeting.TimeLimit = timeLimit
|
||||||
|
meeting.JiraURL = jiraURL
|
||||||
return a.store.UpdateMeeting(meeting)
|
return a.store.UpdateMeeting(meeting)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +185,17 @@ func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.participantURLs = make(map[uint]string)
|
||||||
|
if meeting.JiraURL != "" {
|
||||||
|
for _, p := range participants {
|
||||||
|
url := meeting.JiraURL
|
||||||
|
if p.JiraFilter != "" {
|
||||||
|
url = meeting.JiraURL + "&quickFilter=" + p.JiraFilter
|
||||||
|
}
|
||||||
|
a.participantURLs[p.ID] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
|
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
|
||||||
a.timer.SetQueue(queue)
|
a.timer.SetQueue(queue)
|
||||||
a.currentLogs = make(map[uint]*models.ParticipantLog)
|
a.currentLogs = make(map[uint]*models.ParticipantLog)
|
||||||
@@ -205,6 +220,9 @@ func (a *App) handleTimerEvents() {
|
|||||||
runtime.EventsEmit(a.ctx, "timer:meeting_timeup", event.State)
|
runtime.EventsEmit(a.ctx, "timer:meeting_timeup", event.State)
|
||||||
case timer.EventSpeakerChanged:
|
case timer.EventSpeakerChanged:
|
||||||
a.saveSpeakerLog(event.State)
|
a.saveSpeakerLog(event.State)
|
||||||
|
if url, ok := a.participantURLs[event.State.CurrentSpeakerID]; ok && url != "" {
|
||||||
|
runtime.BrowserOpenURL(a.ctx, url)
|
||||||
|
}
|
||||||
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
|
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
|
||||||
case timer.EventMeetingEnded:
|
case timer.EventMeetingEnded:
|
||||||
a.finalizeSpeakerLogs(event.State)
|
a.finalizeSpeakerLogs(event.State)
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Participant struct {
|
type Participant struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
Name string `json:"name" gorm:"not null"`
|
Name string `json:"name" gorm:"not null"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
TimeLimit int `json:"timeLimit" gorm:"default:120"` // seconds
|
JiraFilter string `json:"jiraFilter,omitempty"`
|
||||||
Order int `json:"order" gorm:"default:0"`
|
TimeLimit int `json:"timeLimit" gorm:"default:120"` // seconds
|
||||||
Active bool `json:"active" gorm:"default:true"`
|
Order int `json:"order" gorm:"default:0"`
|
||||||
CreatedAt time.Time `json:"createdAt" tsType:"string"`
|
Active bool `json:"active" gorm:"default:true"`
|
||||||
UpdatedAt time.Time `json:"updatedAt" tsType:"string"`
|
CreatedAt time.Time `json:"createdAt" tsType:"string"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt" tsType:"string"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Meeting struct {
|
type Meeting struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
Name string `json:"name" gorm:"not null;default:Daily Standup"`
|
Name string `json:"name" gorm:"not null;default:Daily Standup"`
|
||||||
|
JiraURL string `json:"jiraUrl,omitempty"`
|
||||||
TimeLimit int `json:"timeLimit" gorm:"default:3600"` // total meeting limit in seconds (1 hour)
|
TimeLimit int `json:"timeLimit" gorm:"default:3600"` // total meeting limit in seconds (1 hour)
|
||||||
Sessions []MeetingSession `json:"sessions,omitempty" gorm:"foreignKey:MeetingID"`
|
Sessions []MeetingSession `json:"sessions,omitempty" gorm:"foreignKey:MeetingID"`
|
||||||
CreatedAt time.Time `json:"createdAt" tsType:"string"`
|
CreatedAt time.Time `json:"createdAt" tsType:"string"`
|
||||||
@@ -71,4 +73,5 @@ type Settings struct {
|
|||||||
WindowFullHeight bool `json:"windowFullHeight" gorm:"default:true"` // use full screen height
|
WindowFullHeight bool `json:"windowFullHeight" gorm:"default:true"` // use full screen height
|
||||||
WindowX int `json:"windowX" gorm:"default:-1"` // -1 = not set (center)
|
WindowX int `json:"windowX" gorm:"default:-1"` // -1 = not set (center)
|
||||||
WindowY int `json:"windowY" gorm:"default:-1"` // -1 = not set (center)
|
WindowY int `json:"windowY" gorm:"default:-1"` // -1 = not set (center)
|
||||||
|
DefaultJiraUrl string `json:"defaultJiraUrl" gorm:"default:''"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,9 +117,10 @@ func (s *Storage) CreateParticipant(p *models.Participant) error {
|
|||||||
|
|
||||||
func (s *Storage) UpdateParticipant(p *models.Participant) error {
|
func (s *Storage) UpdateParticipant(p *models.Participant) error {
|
||||||
return s.db.Model(&models.Participant{}).Where("id = ?", p.ID).Updates(map[string]interface{}{
|
return s.db.Model(&models.Participant{}).Where("id = ?", p.ID).Updates(map[string]interface{}{
|
||||||
"name": p.Name,
|
"name": p.Name,
|
||||||
"email": p.Email,
|
"email": p.Email,
|
||||||
"time_limit": p.TimeLimit,
|
"jira_filter": p.JiraFilter,
|
||||||
|
"time_limit": p.TimeLimit,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user