402 lines
12 KiB
Vue
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>
|
|
|
|
|