| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844 |
- <template>
- <div class="excel-accounting-section">
- <div class="section-header">
- <h2>Excel-pohjan Kirjanpito</h2>
- <div class="header-actions">
- <button class="btn btn-primary" @click="showAddEntryModal">
- <i class="fas fa-plus"></i> Lisää Tapahtuma
- </button>
- <button class="btn btn-secondary" @click="exportToExcel">
- <i class="fas fa-download"></i> Vie Excel
- </button>
- </div>
- </div>
- <!-- Filters -->
- <div class="filters">
- <div class="filter-group">
- <label for="dateFilter">Päivämäärä:</label>
- <input
- type="date"
- id="dateFilter"
- v-model="filters.date"
- @change="applyFilters"
- >
- </div>
- <div class="filter-group">
- <label for="typeFilter">Tyyppi:</label>
- <select id="typeFilter" v-model="filters.type" @change="applyFilters">
- <option value="">Kaikki</option>
- <option value="Tulo">Tulo</option>
- <option value="Kulu">Kulu</option>
- </select>
- </div>
- <div class="filter-group">
- <label for="categoryFilter">Luokka:</label>
- <input
- type="text"
- id="categoryFilter"
- v-model="filters.category"
- placeholder="Hae luokasta..."
- @input="applyFilters"
- >
- </div>
- </div>
- <!-- Summary Cards -->
- <div class="summary-cards">
- <div class="summary-card income">
- <h3>Tulot</h3>
- <p class="amount">{{ formatCurrency(totalIncome) }}</p>
- </div>
- <div class="summary-card expenses">
- <h3>Kulut</h3>
- <p class="amount">{{ formatCurrency(totalExpenses) }}</p>
- </div>
- <div class="summary-card balance" :class="{ 'positive': netBalance >= 0, 'negative': netBalance < 0 }">
- <h3>Saldo</h3>
- <p class="amount">{{ formatCurrency(netBalance) }}</p>
- </div>
- </div>
- <!-- Entries Table -->
- <div class="entries-section">
- <div v-if="loading" class="loading">
- Ladataan tapahtumia...
- </div>
-
- <div v-else-if="filteredEntries.length === 0" class="no-data">
- Ei tapahtumia löytynyt.
- </div>
-
- <table v-else class="table">
- <thead>
- <tr>
- <th>Päivämäärä</th>
- <th>Kuvaus</th>
- <th>Tyyppi</th>
- <th>Luokka</th>
- <th>Veroton summa</th>
- <th>ALV %</th>
- <th>ALV 25,5%</th>
- <th>ALV 14%</th>
- <th>ALV 10%</th>
- <th>Yhteensä</th>
- <th>Netto</th>
- <th>ALV</th>
- <th>Viite</th>
- <th>Toiminnot</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="entry in paginatedEntries" :key="entry.id" :class="{ 'income': entry.entry_type === 'Tulo', 'expense': entry.entry_type === 'Kulu' }">
- <td>{{ formatDate(entry.entry_date) }}</td>
- <td>{{ entry.description }}</td>
- <td>
- <span :class="['entry-type-badge', entry.entry_type.toLowerCase()]">
- {{ entry.entry_type }}
- </span>
- </td>
- <td>{{ entry.category || '-' }}</td>
- <td class="currency">{{ formatCurrency(entry.tax_free_amount) }}</td>
- <td class="percentage">{{ entry.vat_percentage }}%</td>
- <td class="currency">{{ formatCurrency(entry.vat_25_5) }}</td>
- <td class="currency">{{ formatCurrency(entry.vat_14) }}</td>
- <td class="currency">{{ formatCurrency(entry.vat_10) }}</td>
- <td class="currency">{{ formatCurrency(entry.total_amount) }}</td>
- <td class="currency">{{ formatCurrency(entry.net_amount) }}</td>
- <td class="currency">{{ formatCurrency(entry.vat_amount) }}</td>
- <td>{{ entry.reference_number || '-' }}</td>
- <td>
- <div class="actions">
- <button class="btn btn-sm btn-primary" @click="editEntry(entry)">Muokkaa</button>
- <button class="btn btn-sm btn-danger" @click="deleteEntry(entry.id)">Poista</button>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- <!-- Pagination -->
- <div class="pagination" v-if="totalPages > 1">
- <button
- class="btn btn-secondary"
- @click="currentPage--"
- :disabled="currentPage <= 1"
- >
- Edellinen
- </button>
- <span class="page-info">Sivu {{ currentPage }} / {{ totalPages }}</span>
- <button
- class="btn btn-secondary"
- @click="currentPage++"
- :disabled="currentPage >= totalPages"
- >
- Seuraava
- </button>
- </div>
- </div>
- <!-- Add/Edit Entry Modal -->
- <div v-if="showEntryModal" class="modal" @click.self="closeEntryModal">
- <div class="modal-content">
- <span class="close" @click="closeEntryModal">×</span>
- <h2>{{ isEditingEntry ? 'Muokkaa Tapahtumaa' : 'Lisää Tapahtuma' }}</h2>
-
- <form @submit.prevent="saveEntry">
- <div class="form-row">
- <div class="form-group">
- <label for="entry_date">Päivämäärä *</label>
- <input
- type="date"
- id="entry_date"
- v-model="entryForm.entry_date"
- required
- >
- </div>
- <div class="form-group">
- <label for="entry_type">Tyyppi *</label>
- <select id="entry_type" v-model="entryForm.entry_type" @change="onEntryTypeChange" required>
- <option value="">Valitse tyyppi</option>
- <option value="Tulo">Tulo</option>
- <option value="Kulu">Kulu</option>
- </select>
- </div>
- </div>
- <div class="form-group">
- <label for="description">Kuvaus *</label>
- <textarea
- id="description"
- v-model="entryForm.description"
- required
- placeholder="Syötä tapahtuman kuvaus..."
- ></textarea>
- </div>
- <div class="form-group">
- <label for="category">Luokka</label>
- <select
- id="category"
- v-model="entryForm.category"
- @change="onCategoryChange"
- >
- <option value="">Valitse luokka</option>
- <option v-for="cat in availableCategories" :key="cat.code" :value="cat.code">
- {{ cat.name }}
- </option>
- </select>
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="tax_free_amount">Veroton summa</label>
- <input
- type="number"
- id="tax_free_amount"
- v-model="entryForm.tax_free_amount"
- step="0.01"
- min="0"
- @input="calculateTotals"
- >
- </div>
- <div class="form-group">
- <label for="vat_percentage">ALV %</label>
- <select id="vat_percentage" v-model="entryForm.vat_percentage" @change="calculateTotals">
- <option value="0">0%</option>
- <option value="10">10%</option>
- <option value="14">14%</option>
- <option value="25.5">25.5%</option>
- </select>
- </div>
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="total_amount">Yhteensä *</label>
- <input
- type="number"
- id="total_amount"
- v-model="entryForm.total_amount"
- step="0.01"
- required
- @input="calculateVAT"
- >
- </div>
- <div class="form-group">
- <label for="reference_number">Viitenumero</label>
- <input
- type="text"
- id="reference_number"
- v-model="entryForm.reference_number"
- placeholder="Viitenumero..."
- >
- </div>
- </div>
- <div class="form-actions">
- <button type="button" class="btn" @click="closeEntryModal">Peruuta</button>
- <button type="submit" class="btn btn-success">
- {{ isEditingEntry ? 'Päivitä' : 'Tallenna' }}
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
- </template>
- <script>
- import api from '../../api/axios'
- import { formatCurrency, formatDate } from '../../utils/locale'
- export default {
- name: 'ExcelAccountingSection',
- emits: ['add-entry', 'edit-entry', 'delete-entry'],
- data() {
- return {
- entries: [],
- loading: false,
- showEntryModal: false,
- isEditingEntry: false,
- currentPage: 1,
- itemsPerPage: 50,
- categories: [],
- categoriesLoading: false,
- filters: {
- date: '',
- type: '',
- category: ''
- },
- entryForm: {
- id: null,
- entry_date: '',
- description: '',
- entry_type: '',
- category: '',
- tax_free_amount: 0,
- vat_percentage: 24,
- vat_25_5: 0,
- vat_14: 0,
- vat_10: 0,
- total_amount: 0,
- net_amount: 0,
- vat_amount: 0,
- reference_number: ''
- }
- }
- },
- computed: {
- availableCategories() {
- if (!this.entryForm.entry_type) {
- return []
- }
- return this.categories.filter(cat => cat.type === this.entryForm.entry_type)
- },
- filteredEntries() {
- let filtered = this.entries
-
- if (this.filters.date) {
- filtered = filtered.filter(entry => entry.entry_date >= this.filters.date)
- }
-
- if (this.filters.type) {
- filtered = filtered.filter(entry => entry.entry_type === this.filters.type)
- }
-
- if (this.filters.category) {
- filtered = filtered.filter(entry =>
- entry.category && entry.category.toLowerCase().includes(this.filters.category.toLowerCase())
- )
- }
-
- return filtered
- },
- paginatedEntries() {
- const start = (this.currentPage - 1) * this.itemsPerPage
- const end = start + this.itemsPerPage
- return this.filteredEntries.slice(start, end)
- },
- totalPages() {
- return Math.ceil(this.filteredEntries.length / this.itemsPerPage)
- },
- totalIncome() {
- return this.filteredEntries
- .filter(entry => entry.entry_type === 'Tulo')
- .reduce((sum, entry) => sum + parseFloat(entry.total_amount || 0), 0)
- },
- totalExpenses() {
- return this.filteredEntries
- .filter(entry => entry.entry_type === 'Kulu')
- .reduce((sum, entry) => sum + parseFloat(entry.total_amount || 0), 0)
- },
- netBalance() {
- return this.totalIncome - this.totalExpenses
- }
- },
- methods: {
- formatCurrency,
- formatDate,
- async fetchEntries() {
- this.loading = true
- try {
- const response = await api.get('/accounting_entries.php')
- this.entries = response.data.records || []
- } catch (error) {
- console.error('Error fetching entries:', error)
- } finally {
- this.loading = false
- }
- },
- async fetchCategories() {
- this.categoriesLoading = true
- try {
- const response = await api.get('/accounting_categories.php')
- this.categories = response.data || []
- } catch (error) {
- console.error('Error fetching categories:', error)
- } finally {
- this.categoriesLoading = false
- }
- },
- showAddEntryModal() {
- this.isEditingEntry = false
- this.entryForm = {
- id: null,
- entry_date: new Date().toISOString().split('T')[0],
- description: '',
- entry_type: '',
- category: '',
- tax_free_amount: 0,
- vat_percentage: 24,
- vat_25_5: 0,
- vat_14: 0,
- vat_10: 0,
- total_amount: 0,
- net_amount: 0,
- vat_amount: 0,
- reference_number: ''
- }
- this.showEntryModal = true
- },
- onCategoryChange() {
- // Automatically set VAT percentage based on selected category
- if (this.entryForm.category) {
- const selectedCategory = this.categories.find(cat => cat.code === this.entryForm.category)
- if (selectedCategory) {
- this.entryForm.vat_percentage = parseFloat(selectedCategory.vat_percentage) || 0
- // Always calculate VAT when category changes to show the calculation
- this.calculateVAT()
- }
- }
- },
- onEntryTypeChange() {
- // Clear category when entry type changes
- this.entryForm.category = ''
- },
- editEntry(entry) {
- this.isEditingEntry = true
- this.entryForm = { ...entry }
- this.showEntryModal = true
- },
- closeEntryModal() {
- this.showEntryModal = false
- this.entryForm = {
- id: null,
- entry_date: '',
- description: '',
- entry_type: '',
- category: '',
- tax_free_amount: 0,
- vat_percentage: 24,
- vat_25_5: 0,
- vat_14: 0,
- vat_10: 0,
- total_amount: 0,
- net_amount: 0,
- vat_amount: 0,
- reference_number: ''
- }
- },
- calculateVAT() {
- const vatRate = parseFloat(this.entryForm.vat_percentage) || 0
- const total = parseFloat(this.entryForm.total_amount) || 0
-
- if (vatRate === 25.5) {
- // Yhteensä = Verollinen vero (Brutto ÷ 1.255)
- this.entryForm.vat_25_5 = total * 0.255 / 1.255
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = 0
- } else if (vatRate === 14) {
- // Yhteensä = Verollinen vero (Brutto ÷ 1.14)
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = total * 0.14 / 1.14
- this.entryForm.vat_10 = 0
- } else if (vatRate === 10) {
- // Yhteensä = Verollinen vero (Brutto ÷ 1.10)
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = total * 0.10 / 1.10
- } else {
- // Yhteensä = Verollinen vero (Brutto ÷ 1.24)
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = 0
- }
-
- this.entryForm.vat_amount = this.entryForm.vat_25_5 + this.entryForm.vat_14 + this.entryForm.vat_10
- this.entryForm.net_amount = total - this.entryForm.vat_amount
-
- // Calculate Yhteensä (VAT amount) for display
- this.entryForm.yhteensa = this.entryForm.vat_amount
- },
- calculateTotals() {
- const taxFree = parseFloat(this.entryForm.tax_free_amount) || 0
- const vatRate = parseFloat(this.entryForm.vat_percentage) || 0
-
- // Calculate VAT amounts based on tax-free amount and VAT rate
- if (vatRate === 25.5) {
- this.entryForm.vat_25_5 = Math.round((taxFree * 0.255) * 100) / 100
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = 0
- } else if (vatRate === 14) {
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = Math.round((taxFree * 0.14) * 100) / 100
- this.entryForm.vat_10 = 0
- } else if (vatRate === 10) {
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = Math.round((taxFree * 0.10) * 100) / 100
- } else {
- // Default 24% VAT
- this.entryForm.vat_25_5 = 0
- this.entryForm.vat_14 = 0
- this.entryForm.vat_10 = 0
- }
-
- const vat25_5 = parseFloat(this.entryForm.vat_25_5) || 0
- const vat14 = parseFloat(this.entryForm.vat_14) || 0
- const vat10 = parseFloat(this.entryForm.vat_10) || 0
-
- this.entryForm.total_amount = Math.round((taxFree + vat25_5 + vat14 + vat10) * 100) / 100
- this.entryForm.vat_amount = Math.round((vat25_5 + vat14 + vat10) * 100) / 100
- this.entryForm.net_amount = Math.round(taxFree * 100) / 100
- },
- async saveEntry() {
- try {
- if (this.isEditingEntry) {
- await api.put(`/accounting_entries.php?id=${this.entryForm.id}`, this.entryForm)
- } else {
- await api.post('/accounting_entries.php', this.entryForm)
- }
-
- this.closeEntryModal()
- this.fetchEntries()
- } catch (error) {
- console.error('Error saving entry:', error)
- }
- },
- async deleteEntry(id) {
- if (!confirm('Haluatko varmasti poistaa tämän tapahtuman?')) {
- return
- }
-
- try {
- await api.delete(`/accounting_entries.php?id=${id}`)
- this.fetchEntries()
- } catch (error) {
- console.error('Error deleting entry:', error)
- }
- },
- applyFilters() {
- this.currentPage = 1
- },
- exportToExcel() {
- // Implementation for Excel export
- alert('Excel-vienti ominaisuus tulossa pian!')
- }
- },
- mounted() {
- this.fetchEntries()
- this.fetchCategories()
- }
- }
- </script>
- <style scoped>
- .excel-accounting-section {
- padding: 20px;
- }
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- }
- .section-header h2 {
- margin: 0;
- color: white;
- }
- .header-actions {
- display: flex;
- gap: 10px;
- }
- .filters {
- display: flex;
- gap: 15px;
- margin-bottom: 20px;
- padding: 15px;
- background-color: #f8f9fa;
- border-radius: 8px;
- }
- .filter-group {
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
- .filter-group label {
- font-weight: 600;
- color: #495057;
- }
- .filter-group input,
- .filter-group select {
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- }
- .summary-cards {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .summary-card {
- padding: 20px;
- border-radius: 8px;
- text-align: center;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- }
- .summary-card.income {
- background-color: #d4edda;
- border-left: 4px solid #28a745;
- }
- .summary-card.expenses {
- background-color: #f8d7da;
- border-left: 4px solid #dc3545;
- }
- .summary-card.balance {
- background-color: #e2e3e5;
- border-left: 4px solid #6c757d;
- }
- .summary-card.balance.positive {
- border-left-color: #28a745;
- }
- .summary-card.balance.negative {
- border-left-color: #dc3545;
- }
- .summary-card h3 {
- margin: 0 0 10px 0;
- color: #495057;
- }
- .summary-card .amount {
- font-size: 24px;
- font-weight: 700;
- margin: 0;
- color: #333;
- }
- .entries-section {
- background-color: white;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- }
- .table {
- width: 100%;
- border-collapse: collapse;
- margin-bottom: 20px;
- }
- .table th,
- .table td {
- padding: 12px;
- text-align: left;
- border-bottom: 1px solid #dee2e6;
- }
- .table th {
- background-color: #f8f9fa;
- font-weight: 600;
- color: #495057;
- }
- .table tbody tr:hover {
- background-color: #f8f9fa;
- }
- .table tbody tr.income {
- background-color: rgba(40, 167, 69, 0.05);
- color: #333 !important;
- }
- .table tbody tr.expense {
- background-color: rgba(220, 53, 69, 0.05);
- color: #333 !important;
- }
- .currency {
- text-align: right;
- font-family: 'Courier New', monospace;
- font-weight: 600;
- }
- .percentage {
- text-align: center;
- font-weight: 600;
- }
- .entry-type-badge {
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 12px;
- font-weight: 600;
- }
- .entry-type-badge.tulo {
- background-color: #28a745;
- color: white;
- }
- .entry-type-badge.kulu {
- background-color: #dc3545;
- color: white;
- }
- .actions {
- display: flex;
- gap: 5px;
- }
- .btn {
- padding: 6px 12px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- transition: background-color 0.2s ease;
- }
- .btn-sm {
- padding: 4px 8px;
- font-size: 11px;
- }
- .btn-primary {
- background-color: #007bff;
- color: white;
- }
- .btn-primary:hover {
- background-color: #0056b3;
- }
- .btn-secondary {
- background-color: #6c757d;
- color: white;
- }
- .btn-secondary:hover {
- background-color: #545b62;
- }
- .btn-success {
- background-color: #28a745;
- color: white;
- }
- .btn-success:hover {
- background-color: #1e7e34;
- }
- .btn-danger {
- background-color: #dc3545;
- color: white;
- }
- .btn-danger:hover {
- background-color: #c82333;
- }
- .modal {
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0,0,0,0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .modal-content {
- background-color: white;
- padding: 30px;
- border-radius: 8px;
- width: 90%;
- max-width: 600px;
- max-height: 90vh;
- overflow-y: auto;
- position: relative;
- }
- .close {
- position: absolute;
- right: 15px;
- top: 15px;
- font-size: 24px;
- cursor: pointer;
- color: #aaa;
- }
- .form-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 15px;
- }
- .form-group {
- margin-bottom: 15px;
- }
- .form-group label {
- display: block;
- margin-bottom: 5px;
- font-weight: 600;
- color: #495057;
- }
- .form-group input,
- .form-group select,
- .form-group textarea {
- width: 100%;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- }
- .form-group textarea {
- min-height: 80px;
- resize: vertical;
- }
- .form-actions {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
- margin-top: 20px;
- }
- .loading {
- text-align: center;
- padding: 40px;
- color: #333;
- }
- .no-data {
- text-align: center;
- padding: 40px;
- color: #6c757d;
- }
- .pagination {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 15px;
- margin-top: 20px;
- }
- .page-info {
- color: #6c757d;
- font-weight: 600;
- }
- </style>
|