Files
BarangaySystem/resources/js/Pages/AddTransaction.vue
2026-06-06 18:43:00 +08:00

402 lines
12 KiB
Vue

<script setup>
import { ref, onMounted, watch } from 'vue'
import { usePageTitle } from '../composables/Core/usePageTitle'
import { useNavigate } from '../composables/Core/useNavigate'
import { useModal } from '../composables/Core/useModal'
import BackButton from '../Components/Core/BackButton.vue'
import axios from 'axios'
usePageTitle('Add Transaction')
const { navigate } = useNavigate()
const modal = useModal()
// State
const isLoading = ref(false)
const isSubmitting = ref(false)
const stores = ref([])
const products = ref([])
const transactionTypes = ref([])
const errors = ref({})
const showSuccessAnimation = ref(false)
const showSuccessState = ref(false)
const form = ref({
scope: 'global', // 'global' or 'store'
store_hash: '',
product_hash: '',
type: '',
amount: '',
description: '',
status: 'completed'
})
// Initialize
onMounted(async () => {
fetchInitialData()
})
const fetchInitialData = async () => {
isLoading.value = true
try {
// Fetch Stores
const storesResponse = await axios.post('/ListStores/MyStores/data')
if (storesResponse.data && Array.isArray(storesResponse.data)) {
stores.value = storesResponse.data
}
// Fetch Transaction Types
const typesResponse = await axios.post('/admin/transactions/types')
if (typesResponse.data && Array.isArray(typesResponse.data)) {
transactionTypes.value = typesResponse.data
}
} catch (error) {
console.error('Error fetching initial data:', error)
} finally {
isLoading.value = false
}
}
// Watchers
watch(() => form.value.store_hash, async (newStoreHash) => {
if (newStoreHash) {
fetchStoreProducts(newStoreHash)
} else {
products.value = []
form.value.product_hash = ''
}
})
watch(() => form.value.scope, (newScope) => {
if (newScope === 'global') {
form.value.store_hash = ''
form.value.product_hash = ''
}
})
const fetchStoreProducts = async (storeHash) => {
try {
const response = await axios.post('/View/Store/Details/data', { target: storeHash })
if (response.data && response.data.products) {
products.value = response.data.products
}
} catch (error) {
console.error('Error fetching products:', error)
}
}
// Form Submission
const submitForm = async () => {
isSubmitting.value = true
errors.value = {}
try {
const payload = {
amount: form.value.amount,
type: form.value.type,
description: form.value.description,
status: form.value.status
}
if (form.value.scope === 'store') {
payload.store_hash = form.value.store_hash
if (form.value.product_hash) {
payload.product_hash = form.value.product_hash
}
}
const response = await axios.post('/admin/transactions/create', payload)
if (response.data && response.data.success) {
// Success!
showSuccessState.value = true
showSuccessAnimation.value = true
setTimeout(() => {
showSuccessAnimation.value = false
navigate({ page: 'ManageGlobalTransactions' })
}, 2000)
}
} catch (error) {
if (error.response && error.response.data && error.response.data.errors) {
errors.value = error.response.data.errors
} else {
console.error('Error submitting transaction:', error)
modal.open({
title: 'Error',
body: 'Failed to save transaction. Please try again.'
})
}
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<div class="add-transaction-page pb-5">
<br><br>
<div class="tf-container">
<div class="d-flex justify-content-between align-items-center mb-4">
<BackButton />
<h4 class="fw_6 mb-0">Record New Transaction</h4>
</div>
<div class="card shadow-lg border-0 overflow-hidden glass-card">
<div class="card-header bg-gradient-primary text-white p-4">
<h5 class="mb-0"><i class="fas fa-plus-circle me-2"></i> Transaction Details</h5>
<p class="mb-0 text-white-50 small">Enter the transaction details below to record it in the system.</p>
</div>
<div class="card-body p-4">
<form @submit.prevent="submitForm">
<!-- Scope Selection -->
<div class="mb-4">
<label class="form-label fw_6 d-block mb-3">Transaction Scope</label>
<div class="scope-toggle-container shadow-sm rounded-pill overflow-hidden p-1">
<input
type="radio"
class="btn-check"
name="scope"
id="scope-global"
value="global"
v-model="form.scope"
>
<label class="btn btn-outline-primary border-0 rounded-pill py-2" for="scope-global">
<i class="fas fa-globe me-1"></i> Global
</label>
<input
type="radio"
class="btn-check"
name="scope"
id="scope-store"
value="store"
v-model="form.scope"
>
<label class="btn btn-outline-primary border-0 rounded-pill py-2" for="scope-store">
<i class="fas fa-store me-1"></i> Store Specific
</label>
</div>
</div>
<div class="row">
<!-- Store Selection (Conditional) -->
<div v-if="form.scope === 'store'" class="col-md-6 mb-3">
<label class="form-label fw_6">Select Store</label>
<select
v-model="form.store_hash"
class="form-select form-select-lg highlight-focus"
:class="{ 'is-invalid': errors.store_hash }"
required
>
<option value="">-- Choose a Store --</option>
<option v-for="store in stores" :key="store.hashkey" :value="store.hashkey">
{{ store.name }}
</option>
</select>
<div v-if="errors.store_hash" class="invalid-feedback">{{ errors.store_hash[0] }}</div>
</div>
<!-- Product Selection (Conditional & Optional) -->
<div v-if="form.scope === 'store'" class="col-md-6 mb-3">
<label class="form-label fw_6">Related Product (Optional)</label>
<select
v-model="form.product_hash"
class="form-select form-select-lg highlight-focus"
:disabled="!form.store_hash"
>
<option value="">-- No Specific Product --</option>
<option v-for="product in products" :key="product.hashkey" :value="product.hashkey">
{{ product.name }}
</option>
</select>
</div>
<!-- Transaction Type -->
<div class="col-md-6 mb-3">
<label class="form-label fw_6">Transaction Type</label>
<select
v-model="form.type"
class="form-select form-select-lg highlight-focus"
:class="{ 'is-invalid': errors.type }"
required
>
<option value="">-- Choose Type --</option>
<option v-for="type in transactionTypes" :key="type.value" :value="type.value">
{{ type.label }}
</option>
</select>
<div v-if="errors.type" class="invalid-feedback">{{ errors.type[0] }}</div>
</div>
<!-- Amount -->
<div class="col-md-6 mb-3">
<label class="form-label fw_6">Amount (PHP)</label>
<div class="input-group input-group-lg custom-input-group">
<span class="input-group-text"></span>
<input
type="number"
step="0.01"
v-model="form.amount"
class="form-control highlight-focus"
:class="{ 'is-invalid': errors.amount }"
placeholder="0.00"
required
>
</div>
<div v-if="errors.amount" class="text-danger small mt-1">{{ errors.amount[0] }}</div>
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label class="form-label fw_6">Description / Notes</label>
<textarea
v-model="form.description"
class="form-control highlight-focus"
rows="3"
placeholder="Details about this transaction..."
></textarea>
</div>
<div class="mt-4">
<AnimatedButton
type="submit"
btnClass="btn btn-primary btn-lg w-100 glow-button"
:loading="isSubmitting"
:success="showSuccessState"
>
<i class="fas fa-save me-2"></i> Record Transaction
</AnimatedButton>
</div>
</form>
</div>
</div>
</div>
<div v-if="showSuccessAnimation" class="success-overlay">
<div class="text-center animate-bounce-in">
<LottiePlayer
path="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/11999b7bb57c.json"
:loop="false"
width="200px"
height="200px"
/>
<h3 class="fw_8 mt-3 text-primary headline-gradient">Success!</h3>
<p class="text-muted">Transaction recorded in the ledger.</p>
</div>
</div>
</div>
</template>
<style scoped>
.glass-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
:global(.dark-mode) .glass-card {
background: rgba(31, 34, 40, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bg-gradient-primary {
background: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
.highlight-focus:focus {
border-color: var(--accent-color, #4e73df);
box-shadow: 0 0 0 0.25rem var(--accent-soft, rgba(78, 115, 223, 0.25));
}
.form-select-lg, .form-control-lg {
border-radius: 12px;
}
.glow-button {
border-radius: 15px;
padding: 15px;
transition: all 0.3s ease;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.glow-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(78, 115, 223, 0.4);
}
.btn-check:checked + .btn-outline-primary {
background-color: var(--accent-color, #4e73df);
color: white;
}
.scope-toggle-container {
display: flex;
width: 100%;
background: var(--bg-secondary, #f8f9fa);
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.05));
}
.scope-toggle-container .btn {
flex: 1;
}
.custom-input-group .input-group-text {
background-color: var(--bg-tertiary, #f0f2f5);
color: var(--text-primary);
border-color: var(--border-color);
}
:global(.dark-mode) .form-label {
color: var(--text-primary);
}
.tf-container {
max-width: 800px;
margin: 0 auto;
}
.success-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
:global(.dark-mode) .success-overlay {
background: rgba(18, 20, 24, 0.95);
}
.animate-bounce-in {
animation: bounce-in 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes bounce-in {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.05); opacity: 1; }
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
.headline-gradient {
background: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>