initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
import LoadingSpinner from '@/Components/LoadingSpinner.vue';
import { useAuth } from '@/composables/Core/useAuth';
const props = defineProps({
orgHash: String
});
const { user } = useAuth();
const resolutions = ref([]);
const loading = ref(true);
const showCreateModal = ref(false);
const newResolution = ref({
title: '',
description: ''
});
const fetchResolutions = async () => {
if (!props.orgHash) return;
loading.value = true;
try {
const response = await axios.post('/Governance/Resolutions/List', { org_hash: props.orgHash });
if (response.data.success) {
resolutions.value = response.data.data;
}
} catch (error) {
console.error('Failed to fetch resolutions');
} finally {
loading.value = false;
}
};
const castVote = async (resolutionHash, vote) => {
try {
const response = await axios.post('/Governance/Resolutions/Vote', {
resolution_hash: resolutionHash,
vote: vote
});
if (response.data.success) {
// Refresh to get updated counts
await fetchResolutions();
}
} catch (error) {
console.error('Failed to cast vote', error);
alert(error.response?.data?.message || 'Failed to cast vote');
}
};
const submitResolution = async () => {
try {
const response = await axios.post('/Governance/Resolutions/Create', {
org_hash: props.orgHash,
...newResolution.value
});
if (response.data.success) {
showCreateModal.value = false;
newResolution.value = { title: '', description: '' };
await fetchResolutions();
}
} catch (error) {
console.error('Failed to create resolution');
}
};
onMounted(fetchResolutions);
</script>
<template>
<div class="governance-resolutions mt-3">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw_7 mb-0">Decisions & Resolutions</h5>
<button @click="showCreateModal = true" class="btn btn-primary rounded-pill btn-sm px-3">
<i class="fas fa-plus me-1"></i> Propose Resolution
</button>
</div>
<div v-if="loading" class="text-center py-4">
<LoadingSpinner />
</div>
<div v-else-if="resolutions.length === 0" class="text-center py-5 bg-light rounded-20 border-dashed">
<div class="mb-3 opacity-25">
<i class="fas fa-gavel fa-3x"></i>
</div>
<h6 class="fw_6">No active resolutions</h6>
<p class="text-muted small">Proposed decisions for the cooperative will appear here.</p>
</div>
<div v-else class="resolution-list">
<div v-for="res in resolutions" :key="res.hashkey" class="card border-0 shadow-sm rounded-20 p-3 mb-3 hover-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span :class="['badge rounded-pill px-3 py-1 fw_6 mb-2',
res.status === 'APPROVED' ? 'bg-soft-success text-success' :
res.status === 'RESCINDED' ? 'bg-soft-danger text-danger' :
'bg-soft-primary text-primary']">
{{ res.status }}
</span>
<h6 class="fw_7 mb-1">{{ res.title }}</h6>
</div>
<div class="text-end text-muted smallest">
{{ new Date(res.created_at).toLocaleDateString() }}
</div>
</div>
<p class="text-muted small mb-3">{{ res.description }}</p>
<div class="voting-section bg-light rounded-15 p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="smallest fw_6 text-uppercase text-muted">Current Votes</span>
<div class="d-flex gap-2">
<span class="text-success smallest fw_7"><i class="fas fa-check-circle me-1"></i> {{ res.yes_votes || 0 }}</span>
<span class="text-danger smallest fw_7"><i class="fas fa-times-circle me-1"></i> {{ res.no_votes || 0 }}</span>
<span class="text-muted smallest fw_7"><i class="fas fa-minus-circle me-1"></i> {{ res.abstain_votes || 0 }}</span>
</div>
</div>
<div class="progress rounded-pill mb-3" style="height: 6px;">
<div class="progress-bar bg-success" :style="{ width: ((res.yes_votes || 0) / ((res.yes_votes || 0) + (res.no_votes || 0) + (res.abstain_votes || 1)) * 100) + '%' }"></div>
<div class="progress-bar bg-danger" :style="{ width: ((res.no_votes || 0) / ((res.yes_votes || 0) + (res.no_votes || 0) + (res.abstain_votes || 1)) * 100) + '%' }"></div>
</div>
<div v-if="res.status === 'PROPOSED'" class="voter-actions d-flex gap-2 justify-content-center">
<button @click="castVote(res.hashkey, 'YES')" class="btn btn-soft-success btn-sm rounded-pill px-3 fw_6">
Vote YES
</button>
<button @click="castVote(res.hashkey, 'NO')" class="btn btn-soft-danger btn-sm rounded-pill px-3 fw_6">
Vote NO
</button>
<button @click="castVote(res.hashkey, 'ABSTAIN')" class="btn btn-soft-secondary btn-sm rounded-pill px-3 fw_6">
Abstain
</button>
</div>
</div>
</div>
</div>
<!-- Create Modal -->
<div v-if="showCreateModal" class="custom-modal-overlay" @click.self="showCreateModal = false">
<div class="card border-0 shadow rounded-20 p-4 w-100 mx-3" style="max-width: 500px;">
<h5 class="fw_7 mb-4">Propose Resolution</h5>
<div class="form-group mb-3">
<label class="small fw_6 mb-1">Resolution Title</label>
<input v-model="newResolution.title" type="text" class="form-control rounded-pill px-3 bg-light border-0" placeholder="e.g. Quarterly Dividend Distribution">
</div>
<div class="form-group mb-4">
<label class="small fw_6 mb-1">Description / Details</label>
<textarea v-model="newResolution.description" class="form-control rounded-20 px-3 bg-light border-0" rows="4" placeholder="Detailed explanation..."></textarea>
</div>
<div class="d-flex gap-2">
<button @click="showCreateModal = false" class="btn btn-light rounded-pill flex-fill py-2 fw_6">Cancel</button>
<button @click="submitResolution" class="btn btn-primary rounded-pill flex-fill py-2 fw_7 shadow-sm">Submit Proposal</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.rounded-20 { border-radius: 20px; }
.rounded-15 { border-radius: 15px; }
.bg-soft-primary { background-color: rgba(var(--primary-rgb), 0.1); }
.bg-soft-success { background-color: rgba(40, 167, 69, 0.1); }
.bg-soft-danger { background-color: rgba(220, 53, 69, 0.1); }
.bg-soft-secondary { background-color: rgba(108, 117, 125, 0.1); }
.hover-card { transition: all 0.2s; }
.hover-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important; }
.border-dashed { border: 2px dashed #eee; }
.custom-modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: flex; align-items: center; justify-content: center;
}
.btn-soft-success { background: rgba(40, 167, 69, 0.1); color: #28a745; border: none; }
.btn-soft-danger { background: rgba(220, 53, 69, 0.1); color: #dc3545; border: none; }
.btn-soft-secondary { background: rgba(108, 117, 125, 0.1); color: #6c757d; border: none; }
</style>