popilot 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +892 -0
- package/scaffold/spec-site/package.json +15 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- package/scaffold/spec-site/src/utils/timezone.ts +18 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted } from 'vue'
|
|
3
|
+
import { apiGet, apiPost, apiPatch, isStaticMode } from '@/api/client'
|
|
4
|
+
|
|
5
|
+
interface Reward {
|
|
6
|
+
id: number; member_name: string; type: 'reward' | 'penalty'
|
|
7
|
+
amount: number; reason: string; status: string; issued_by: string
|
|
8
|
+
tx_hash: string | null; paid_at: string | null; created_at: string
|
|
9
|
+
}
|
|
10
|
+
interface Summary {
|
|
11
|
+
member_name: string; total_rewards: number; total_penalties: number
|
|
12
|
+
balance: number; pending_balance: number
|
|
13
|
+
}
|
|
14
|
+
interface Member {
|
|
15
|
+
id: number; display_name: string; wallet_address: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rewards = ref<Reward[]>([])
|
|
19
|
+
const summary = ref<Summary[]>([])
|
|
20
|
+
const members = ref<Member[]>([])
|
|
21
|
+
const loading = ref(true)
|
|
22
|
+
const filterMember = ref('')
|
|
23
|
+
const activeTab = ref<'pending' | 'all'>('pending')
|
|
24
|
+
|
|
25
|
+
// Create form
|
|
26
|
+
const showForm = ref(false)
|
|
27
|
+
const formMember = ref('')
|
|
28
|
+
const formType = ref<'reward' | 'penalty'>('reward')
|
|
29
|
+
const formAmount = ref(0)
|
|
30
|
+
const formReason = ref('')
|
|
31
|
+
|
|
32
|
+
// Wallet editing
|
|
33
|
+
const editingWallet = ref<number | null>(null)
|
|
34
|
+
const walletInput = ref('')
|
|
35
|
+
|
|
36
|
+
// Payment
|
|
37
|
+
const selectedIds = ref<Set<number>>(new Set())
|
|
38
|
+
const txHashInput = ref('')
|
|
39
|
+
|
|
40
|
+
const pendingRewards = computed(() => rewards.value.filter(r => r.status === 'pending'))
|
|
41
|
+
|
|
42
|
+
const filteredRewards = computed(() => {
|
|
43
|
+
let list = activeTab.value === 'pending' ? pendingRewards.value : rewards.value
|
|
44
|
+
if (filterMember.value) list = list.filter(r => r.member_name === filterMember.value)
|
|
45
|
+
return list
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
async function loadData() {
|
|
49
|
+
if (isStaticMode()) { loading.value = false; return }
|
|
50
|
+
loading.value = true
|
|
51
|
+
const [rewardsRes, summaryRes, membersRes] = await Promise.all([
|
|
52
|
+
apiGet('/api/v2/rewards'),
|
|
53
|
+
apiGet('/api/v2/rewards/summary'),
|
|
54
|
+
apiGet('/api/v2/admin/members'),
|
|
55
|
+
])
|
|
56
|
+
if (rewardsRes.data?.rewards) rewards.value = rewardsRes.data.rewards as Reward[]
|
|
57
|
+
if (summaryRes.data?.summary) summary.value = summaryRes.data.summary as Summary[]
|
|
58
|
+
if (membersRes.data?.members) members.value = (membersRes.data.members as Member[]).filter((m: any) => m.is_active)
|
|
59
|
+
loading.value = false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function addReward() {
|
|
63
|
+
if (!formMember.value || !formAmount.value || !formReason.value) return
|
|
64
|
+
await apiPost('/api/v2/rewards', { memberName: formMember.value, type: formType.value, amount: formAmount.value, reason: formReason.value })
|
|
65
|
+
showForm.value = false; formMember.value = ''; formAmount.value = 0; formReason.value = ''
|
|
66
|
+
await loadData()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function paySelected() {
|
|
70
|
+
for (const id of selectedIds.value) {
|
|
71
|
+
await apiPatch(`/api/v2/rewards/${id}/pay`, { txHash: txHashInput.value || null })
|
|
72
|
+
}
|
|
73
|
+
selectedIds.value.clear(); txHashInput.value = ''
|
|
74
|
+
await loadData()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function batchPay(memberName: string) {
|
|
78
|
+
await apiPatch('/api/v2/rewards/batch-pay', { memberName, txHash: txHashInput.value || null })
|
|
79
|
+
txHashInput.value = ''
|
|
80
|
+
await loadData()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function startEditWallet(m: Member) {
|
|
84
|
+
editingWallet.value = m.id; walletInput.value = m.wallet_address || ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function saveWallet(memberId: number) {
|
|
88
|
+
await apiPatch(`/api/v2/admin/members/${memberId}`, { wallet_address: walletInput.value })
|
|
89
|
+
editingWallet.value = null
|
|
90
|
+
await loadData()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toggleSelect(id: number) {
|
|
94
|
+
if (selectedIds.value.has(id)) selectedIds.value.delete(id)
|
|
95
|
+
else selectedIds.value.add(id)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatDate(d: string) {
|
|
99
|
+
if (!d) return ''
|
|
100
|
+
const date = new Date(d.endsWith('Z') ? d : d + 'Z')
|
|
101
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getMemberWallet(name: string) {
|
|
105
|
+
return members.value.find(m => m.display_name === name)?.wallet_address || ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function copyAddr(addr: string | null) {
|
|
109
|
+
if (!addr) return
|
|
110
|
+
navigator.clipboard.writeText(addr)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onMounted(loadData)
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="rewards-page">
|
|
118
|
+
<div class="rewards-header">
|
|
119
|
+
<h1>Rewards & Penalties</h1>
|
|
120
|
+
<button class="btn-add" @click="showForm = !showForm">{{ showForm ? 'Cancel' : '+ Add Entry' }}</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Create form -->
|
|
124
|
+
<div v-if="showForm" class="form-card">
|
|
125
|
+
<div class="form-row">
|
|
126
|
+
<select v-model="formMember" class="input">
|
|
127
|
+
<option value="">Select Member</option>
|
|
128
|
+
<option v-for="m in members" :key="m.id" :value="m.display_name">{{ m.display_name }}</option>
|
|
129
|
+
</select>
|
|
130
|
+
<select v-model="formType" class="input">
|
|
131
|
+
<option value="reward">Reward (+)</option>
|
|
132
|
+
<option value="penalty">Penalty (-)</option>
|
|
133
|
+
</select>
|
|
134
|
+
<input v-model.number="formAmount" class="input" type="number" placeholder="Amount" />
|
|
135
|
+
</div>
|
|
136
|
+
<input v-model="formReason" class="input full" placeholder="Reason" @keydown.enter="addReward" />
|
|
137
|
+
<button class="btn-submit" @click="addReward" :disabled="!formMember || !formAmount || !formReason">Submit</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Member balance cards -->
|
|
141
|
+
<section class="summary-section">
|
|
142
|
+
<h2>Balance by Member</h2>
|
|
143
|
+
<div class="summary-grid">
|
|
144
|
+
<div v-for="s in summary" :key="s.member_name" class="summary-card">
|
|
145
|
+
<div class="summary-top">
|
|
146
|
+
<div class="summary-name">{{ s.member_name }}</div>
|
|
147
|
+
<div class="summary-balance" :class="{ negative: s.balance < 0 }">{{ s.balance.toLocaleString() }}</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="summary-wallet">
|
|
150
|
+
<template v-if="editingWallet === members.find(m => m.display_name === s.member_name)?.id">
|
|
151
|
+
<input v-model="walletInput" class="wallet-input" placeholder="Wallet address" @keydown.enter="saveWallet(members.find(m => m.display_name === s.member_name)!.id)" />
|
|
152
|
+
<button class="btn-sm" @click="saveWallet(members.find(m => m.display_name === s.member_name)!.id)">Save</button>
|
|
153
|
+
</template>
|
|
154
|
+
<template v-else>
|
|
155
|
+
<span class="wallet-addr" @click="copyAddr(getMemberWallet(s.member_name))" :title="getMemberWallet(s.member_name)">{{ getMemberWallet(s.member_name)?.slice(0,12) || 'Not set' }}...</span>
|
|
156
|
+
<button class="btn-edit" @click="startEditWallet(members.find(m => m.display_name === s.member_name)!)">Edit</button>
|
|
157
|
+
</template>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="summary-detail">
|
|
160
|
+
<span class="reward-text">+{{ s.total_rewards.toLocaleString() }}</span>
|
|
161
|
+
<span class="penalty-text">-{{ s.total_penalties.toLocaleString() }}</span>
|
|
162
|
+
<span v-if="s.pending_balance" class="pending-text">Pending: {{ s.pending_balance.toLocaleString() }}</span>
|
|
163
|
+
</div>
|
|
164
|
+
<button v-if="s.pending_balance > 0" class="btn-batch-pay" @click="batchPay(s.member_name)">Pay All</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
|
|
169
|
+
<!-- Tabs -->
|
|
170
|
+
<div class="tab-row">
|
|
171
|
+
<button :class="['tab-btn', { active: activeTab === 'pending' }]" @click="activeTab = 'pending'">Pending</button>
|
|
172
|
+
<button :class="['tab-btn', { active: activeTab === 'all' }]" @click="activeTab = 'all'">All</button>
|
|
173
|
+
<select v-model="filterMember" class="input filter-select">
|
|
174
|
+
<option value="">All Members</option>
|
|
175
|
+
<option v-for="s in summary" :key="s.member_name" :value="s.member_name">{{ s.member_name }}</option>
|
|
176
|
+
</select>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Batch pay bar -->
|
|
180
|
+
<div v-if="selectedIds.size > 0 && activeTab === 'pending'" class="pay-bar">
|
|
181
|
+
<span>{{ selectedIds.size }} selected</span>
|
|
182
|
+
<input v-model="txHashInput" class="input" placeholder="TX hash (optional)" />
|
|
183
|
+
<button class="btn-pay" @click="paySelected">Pay Selected</button>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- List -->
|
|
187
|
+
<div v-if="loading" class="loading">Loading...</div>
|
|
188
|
+
<div v-else-if="!filteredRewards.length" class="empty">No entries found.</div>
|
|
189
|
+
<div v-else class="rewards-list">
|
|
190
|
+
<div v-for="r in filteredRewards" :key="r.id" class="reward-item" :class="'type--' + r.type">
|
|
191
|
+
<input v-if="activeTab === 'pending'" type="checkbox" :checked="selectedIds.has(r.id)" @change="toggleSelect(r.id)" class="reward-check" />
|
|
192
|
+
<div class="reward-main">
|
|
193
|
+
<span class="reward-type-badge">{{ r.type === 'reward' ? 'Reward' : 'Penalty' }}</span>
|
|
194
|
+
<span class="reward-member">{{ r.member_name }}</span>
|
|
195
|
+
<span class="reward-amount" :class="r.type">{{ r.type === 'reward' ? '+' : '-' }}{{ r.amount.toLocaleString() }}</span>
|
|
196
|
+
<span class="reward-status" :class="'st--' + r.status">{{ r.status === 'paid' ? 'Paid' : 'Pending' }}</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="reward-reason">{{ r.reason }}</div>
|
|
199
|
+
<div class="reward-meta">
|
|
200
|
+
<span>{{ r.issued_by }}</span>
|
|
201
|
+
<span>{{ formatDate(r.created_at) }}</span>
|
|
202
|
+
<a v-if="r.tx_hash" class="tx-hash" :href="`https://tronscan.org/#/transaction/${r.tx_hash}`" target="_blank">TX: {{ r.tx_hash.slice(0, 12) }}...</a>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</template>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
.rewards-page { max-width: 800px; margin: 0 auto; padding: 24px 16px; }
|
|
211
|
+
.rewards-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
212
|
+
.rewards-header h1 { font-size: 20px; font-weight: 700; }
|
|
213
|
+
.btn-add { background: #3b82f6; color: #fff; border: none; border-radius: 10px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
214
|
+
|
|
215
|
+
.form-card { background: var(--card-bg, #fff); border-radius: 16px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
|
216
|
+
.form-row { display: flex; gap: 8px; margin-bottom: 8px; }
|
|
217
|
+
.input { border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; padding: 8px 12px; font-size: 13px; }
|
|
218
|
+
.input.full { width: 100%; margin-bottom: 8px; box-sizing: border-box; }
|
|
219
|
+
.btn-submit { background: #22c55e; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; font-size: 13px; cursor: pointer; }
|
|
220
|
+
|
|
221
|
+
.summary-section { margin-bottom: 24px; }
|
|
222
|
+
.summary-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; }
|
|
223
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
224
|
+
.summary-card { background: var(--card-bg, #fff); border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
|
225
|
+
.summary-top { display: flex; justify-content: space-between; align-items: center; }
|
|
226
|
+
.summary-name { font-size: 14px; font-weight: 600; }
|
|
227
|
+
.summary-balance { font-size: 18px; font-weight: 700; color: #22c55e; }
|
|
228
|
+
.summary-balance.negative { color: #ef4444; }
|
|
229
|
+
.summary-wallet { display: flex; align-items: center; gap: 4px; margin-top: 8px; font-size: 11px; color: var(--text-muted, #888); }
|
|
230
|
+
.wallet-input { flex: 1; font-size: 11px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px; }
|
|
231
|
+
.wallet-addr { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; cursor: pointer; }
|
|
232
|
+
.wallet-addr:hover { color: #3b82f6; }
|
|
233
|
+
.btn-edit { background: none; border: none; color: #3b82f6; font-size: 11px; cursor: pointer; }
|
|
234
|
+
.btn-sm { background: #3b82f6; color: #fff; border: none; border-radius: 4px; padding: 2px 6px; font-size: 10px; cursor: pointer; }
|
|
235
|
+
.summary-detail { font-size: 11px; color: var(--text-muted, #888); margin-top: 4px; display: flex; gap: 8px; }
|
|
236
|
+
.reward-text { color: #22c55e; } .penalty-text { color: #ef4444; } .pending-text { color: #f59e0b; }
|
|
237
|
+
.btn-batch-pay { width: 100%; margin-top: 8px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; padding: 6px; font-size: 12px; cursor: pointer; }
|
|
238
|
+
|
|
239
|
+
.tab-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
|
240
|
+
.tab-btn { background: #f3f4f6; border: none; border-radius: 8px; padding: 6px 14px; font-size: 13px; cursor: pointer; }
|
|
241
|
+
.tab-btn.active { background: #3b82f6; color: #fff; }
|
|
242
|
+
.filter-select { margin-left: auto; }
|
|
243
|
+
|
|
244
|
+
.pay-bar { display: flex; gap: 8px; align-items: center; background: #eff6ff; padding: 8px 12px; border-radius: 8px; margin-bottom: 12px; font-size: 13px; }
|
|
245
|
+
.btn-pay { background: #22c55e; color: #fff; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; cursor: pointer; }
|
|
246
|
+
|
|
247
|
+
.rewards-list { display: flex; flex-direction: column; gap: 6px; }
|
|
248
|
+
.reward-item { display: flex; align-items: flex-start; gap: 8px; background: var(--card-bg, #fff); border-radius: 10px; padding: 10px 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); flex-wrap: wrap; }
|
|
249
|
+
.type--reward { border-left: 3px solid #22c55e; } .type--penalty { border-left: 3px solid #ef4444; }
|
|
250
|
+
.reward-check { margin-top: 4px; }
|
|
251
|
+
.reward-main { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
|
|
252
|
+
.reward-type-badge { font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 4px; }
|
|
253
|
+
.type--reward .reward-type-badge { background: #dcfce7; color: #16a34a; }
|
|
254
|
+
.type--penalty .reward-type-badge { background: #fef2f2; color: #dc2626; }
|
|
255
|
+
.reward-member { font-weight: 600; font-size: 13px; }
|
|
256
|
+
.reward-amount { font-weight: 700; font-size: 14px; margin-left: auto; }
|
|
257
|
+
.reward-amount.reward { color: #22c55e; } .reward-amount.penalty { color: #ef4444; }
|
|
258
|
+
.reward-status { font-size: 10px; padding: 1px 5px; border-radius: 4px; }
|
|
259
|
+
.st--pending { background: #fef3c7; color: #d97706; } .st--paid { background: #dcfce7; color: #16a34a; }
|
|
260
|
+
.reward-reason { font-size: 12px; color: var(--text-secondary, #666); width: 100%; }
|
|
261
|
+
.reward-meta { display: flex; gap: 8px; font-size: 11px; color: var(--text-muted, #888); width: 100%; }
|
|
262
|
+
.tx-hash { font-family: monospace; font-size: 10px; color: #3b82f6; text-decoration: none; }
|
|
263
|
+
.tx-hash:hover { text-decoration: underline; }
|
|
264
|
+
.loading, .empty { text-align: center; color: var(--text-muted, #888); padding: 40px; }
|
|
265
|
+
@media (max-width: 640px) { .form-row { flex-direction: column; } .summary-grid { grid-template-columns: 1fr; } .tab-row { flex-wrap: wrap; } }
|
|
266
|
+
</style>
|