|
|
@@ -0,0 +1,2064 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <header>
|
|
|
+ <div class="container">
|
|
|
+ <!-- Login Section -->
|
|
|
+ <div v-if="!isLoggedIn" class="login-container">
|
|
|
+ <div class="login-card">
|
|
|
+ <h2>Login</h2>
|
|
|
+ <form @submit.prevent="login">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="login_username">Username</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="login_username"
|
|
|
+ v-model="loginForm.username"
|
|
|
+ required
|
|
|
+ placeholder="Enter username"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="login_password">Password</label>
|
|
|
+ <input
|
|
|
+ type="password"
|
|
|
+ id="login_password"
|
|
|
+ v-model="loginForm.password"
|
|
|
+ required
|
|
|
+ placeholder="Enter password"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button type="submit" class="btn btn-primary btn-block">Login</button>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div v-if="loginError" class="error-message">
|
|
|
+ {{ loginError }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Navigation Tabs -->
|
|
|
+ <div class="nav-tabs">
|
|
|
+ <button
|
|
|
+ :class="['nav-tab', { active: activeSection === 'items' }]"
|
|
|
+ @click="activeSection = 'items'"
|
|
|
+ >
|
|
|
+ Items
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :class="['nav-tab', { active: activeSection === 'clients' }]"
|
|
|
+ @click="activeSection = 'clients'"
|
|
|
+ >
|
|
|
+ Clients
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :class="['nav-tab', { active: activeSection === 'accounting' }]"
|
|
|
+ @click="activeSection = 'accounting'"
|
|
|
+ >
|
|
|
+ Accounting
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Items Section -->
|
|
|
+ <div v-if="activeSection === 'items'" class="card">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
+ <h2>Items</h2>
|
|
|
+ <button class="btn btn-primary" @click="showAddModal">Add New Item</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="loading" class="loading">
|
|
|
+ Loading items...
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="items.length === 0" class="loading">
|
|
|
+ No items found. Add your first item!
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table v-else class="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>ID</th>
|
|
|
+ <th>Name</th>
|
|
|
+ <th>Description</th>
|
|
|
+ <th>Serial Number</th>
|
|
|
+ <th>Picture</th>
|
|
|
+ <th>Quantity</th>
|
|
|
+ <th>Price</th>
|
|
|
+ <th>Created</th>
|
|
|
+ <th>Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="item in items" :key="item.id">
|
|
|
+ <td>{{ item.id }}</td>
|
|
|
+ <td>{{ item.name }}</td>
|
|
|
+ <td>{{ item.description || '-' }}</td>
|
|
|
+ <td>{{ item.serial_number || '-' }}</td>
|
|
|
+ <td>
|
|
|
+ <img v-if="item.picture" :src="item.picture" :alt="item.name" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
|
|
|
+ <span v-else>-</span>
|
|
|
+ </td>
|
|
|
+ <td>{{ item.quantity }}</td>
|
|
|
+ <td>${{ parseFloat(item.price).toFixed(2) }}</td>
|
|
|
+ <td>{{ formatDate(item.created_at) }}</td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button class="btn btn-primary" @click="viewItemDetails(item)">Details</button>
|
|
|
+ <button class="btn btn-warning" @click="editItem(item)">Edit</button>
|
|
|
+ <button class="btn btn-danger" @click="deleteItem(item.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Clients Section -->
|
|
|
+ <div v-if="activeSection === 'clients'" class="card">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
+ <h2>Clients</h2>
|
|
|
+ <div style="display: flex; gap: 10px;">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ v-model="clientSearch"
|
|
|
+ placeholder="Search clients..."
|
|
|
+ @input="searchClients"
|
|
|
+ style="padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
|
|
|
+ >
|
|
|
+ <button class="btn btn-primary" @click="showAddClientModal">Add New Client</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="clientsLoading" class="loading">
|
|
|
+ Loading clients...
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="clients.length === 0" class="loading">
|
|
|
+ No clients found. Add your first client!
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table v-else class="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>ID</th>
|
|
|
+ <th>Name</th>
|
|
|
+ <th>Company</th>
|
|
|
+ <th>Email</th>
|
|
|
+ <th>Phone</th>
|
|
|
+ <th>City</th>
|
|
|
+ <th>Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="client in clients" :key="client.id">
|
|
|
+ <td>{{ client.id }}</td>
|
|
|
+ <td>{{ client.first_name }} {{ client.last_name }}</td>
|
|
|
+ <td>{{ client.company_name || '-' }}</td>
|
|
|
+ <td>{{ client.email }}</td>
|
|
|
+ <td>{{ client.phone || '-' }}</td>
|
|
|
+ <td>{{ client.city || '-' }}</td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button class="btn btn-warning" @click="editClient(client)">Edit</button>
|
|
|
+ <button class="btn btn-danger" @click="deleteClient(client.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add/Edit Modal -->
|
|
|
+ <div v-if="showModal" class="modal" @click.self="closeModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeModal">×</span>
|
|
|
+ <h2>{{ isEditing ? 'Edit Item' : 'Add New Item' }}</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveItem">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="name">Name *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="name"
|
|
|
+ v-model="formData.name"
|
|
|
+ required
|
|
|
+ placeholder="Enter item name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="description">Description</label>
|
|
|
+ <textarea
|
|
|
+ id="description"
|
|
|
+ v-model="formData.description"
|
|
|
+ placeholder="Enter item description"
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="serial_number">Serial Number</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="serial_number"
|
|
|
+ v-model="formData.serial_number"
|
|
|
+ placeholder="Enter serial number"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="picture">Picture</label>
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ id="picture"
|
|
|
+ @change="handleFileUpload"
|
|
|
+ accept="image/*"
|
|
|
+ >
|
|
|
+ <div v-if="formData.picture" style="margin-top: 10px;">
|
|
|
+ <img :src="formData.picture" :alt="formData.name" style="max-width: 200px; max-height: 200px; border-radius: 4px;">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="quantity">Quantity *</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="quantity"
|
|
|
+ v-model="formData.quantity"
|
|
|
+ required
|
|
|
+ min="0"
|
|
|
+ placeholder="Enter quantity"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="price">Price *</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="price"
|
|
|
+ v-model="formData.price"
|
|
|
+ required
|
|
|
+ min="0"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter price"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">
|
|
|
+ {{ isEditing ? 'Update' : 'Add' }} Item
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Item Details Modal -->
|
|
|
+ <div v-if="showDetailsModal" class="modal" @click.self="closeDetailsModal">
|
|
|
+ <div class="modal-content" style="max-width: 800px;">
|
|
|
+ <span class="close" @click="closeDetailsModal">×</span>
|
|
|
+ <h2>Item Details: {{ selectedItem.name }}</h2>
|
|
|
+
|
|
|
+ <div class="tabs">
|
|
|
+ <button
|
|
|
+ :class="['tab-btn', { active: activeTab === 'info' }]"
|
|
|
+ @click="activeTab = 'info'"
|
|
|
+ >
|
|
|
+ Information
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :class="['tab-btn', { active: activeTab === 'rental' }]"
|
|
|
+ @click="activeTab = 'rental'"
|
|
|
+ >
|
|
|
+ Rental Prices
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :class="['tab-btn', { active: activeTab === 'attachments' }]"
|
|
|
+ @click="activeTab = 'attachments'"
|
|
|
+ >
|
|
|
+ Attachments
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ :class="['tab-btn', { active: activeTab === 'contacts' }]"
|
|
|
+ @click="activeTab = 'contacts'"
|
|
|
+ >
|
|
|
+ Contact Persons
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Information Tab -->
|
|
|
+ <div v-if="activeTab === 'info'" class="tab-content">
|
|
|
+ <div class="item-info">
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>ID:</strong> {{ selectedItem.id }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Name:</strong> {{ selectedItem.name }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Description:</strong> {{ selectedItem.description || 'No description' }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Serial Number:</strong> {{ selectedItem.serial_number || 'No serial number' }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Quantity:</strong> {{ selectedItem.quantity }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Price:</strong> ${{ parseFloat(selectedItem.price).toFixed(2) }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <strong>Created:</strong> {{ formatDate(selectedItem.created_at) }}
|
|
|
+ </div>
|
|
|
+ <div class="info-row" v-if="selectedItem.picture">
|
|
|
+ <strong>Picture:</strong><br>
|
|
|
+ <img :src="selectedItem.picture" :alt="selectedItem.name" style="max-width: 300px; border-radius: 4px; margin-top: 10px;">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Rental Prices Tab -->
|
|
|
+ <div v-if="activeTab === 'rental'" class="tab-content">
|
|
|
+ <div style="margin-bottom: 20px;">
|
|
|
+ <button class="btn btn-primary" @click="showAddRentalModal">Add Rental Price</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="rentalPrices.length === 0" class="loading">
|
|
|
+ No rental prices found.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table v-else class="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>Client</th>
|
|
|
+ <th>Start Date</th>
|
|
|
+ <th>End Date</th>
|
|
|
+ <th>Daily Price</th>
|
|
|
+ <th>Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="rental in rentalPrices" :key="rental.id">
|
|
|
+ <td>{{ getClientName(rental.client_id) }}</td>
|
|
|
+ <td>{{ formatDate(rental.start_date) }}</td>
|
|
|
+ <td>{{ formatDate(rental.end_date) }}</td>
|
|
|
+ <td>${{ parseFloat(rental.daily_price).toFixed(2) }}</td>
|
|
|
+ <td>
|
|
|
+ <button class="btn btn-danger" @click="deleteRentalPrice(rental.id)">Delete</button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Contact Persons Tab -->
|
|
|
+ <div v-if="activeTab === 'contacts'" class="tab-content">
|
|
|
+ <div style="margin-bottom: 20px;">
|
|
|
+ <button class="btn btn-primary" @click="showAddContactPersonModal">Add Contact Person</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="contactPersons.length === 0" class="loading">
|
|
|
+ No contact persons found.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="contacts-list">
|
|
|
+ <div v-for="person in contactPersons" :key="person.id" class="contact-person-item">
|
|
|
+ <div class="contact-person-info">
|
|
|
+ <strong>{{ person.first_name }} {{ person.last_name }}</strong>
|
|
|
+ <span v-if="person.position" class="contact-position">- {{ person.position }}</span>
|
|
|
+ <span v-if="person.is_primary" class="contact-primary">(Primary)</span>
|
|
|
+ </div>
|
|
|
+ <div class="contact-person-details">
|
|
|
+ <div v-if="person.email">📧 {{ person.email }}</div>
|
|
|
+ <div v-if="person.phone">📞 {{ person.phone }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="contact-person-actions">
|
|
|
+ <button class="btn btn-warning" @click="editContactPerson(person)">Edit</button>
|
|
|
+ <button class="btn btn-danger" @click="deleteContactPerson(person.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Attachments Tab -->
|
|
|
+ <div v-if="activeTab === 'attachments'" class="tab-content">
|
|
|
+ <div style="margin-bottom: 20px;">
|
|
|
+ <button class="btn btn-primary" @click="showAddAttachmentModal">Add Attachment</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="attachments.length === 0" class="loading">
|
|
|
+ No attachments found.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="attachments-list">
|
|
|
+ <div v-for="attachment in attachments" :key="attachment.id" class="attachment-item">
|
|
|
+ <div class="attachment-info">
|
|
|
+ <strong>{{ attachment.original_name }}</strong>
|
|
|
+ <span class="attachment-type">{{ attachment.file_type }}</span>
|
|
|
+ <span class="attachment-size">({{ formatFileSize(attachment.file_size) }})</span>
|
|
|
+ </div>
|
|
|
+ <div class="attachment-actions">
|
|
|
+ <a :href="attachment.file_path" target="_blank" class="btn btn-primary">View</a>
|
|
|
+ <button class="btn btn-danger" @click="deleteAttachment(attachment.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add Rental Price Modal -->
|
|
|
+ <div v-if="showRentalModal" class="modal" @click.self="closeRentalModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeRentalModal">×</span>
|
|
|
+ <h2>Add Rental Price</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveRentalPrice">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="client_id">Client</label>
|
|
|
+ <select id="client_id" v-model="rentalForm.client_id">
|
|
|
+ <option value="">Select client (optional)</option>
|
|
|
+ <option v-for="client in clients" :key="client.id" :value="client.id">
|
|
|
+ {{ client.company_name ? client.company_name + ' - ' : '' }}{{ client.first_name }} {{ client.last_name }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="start_date">Start Date *</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ id="start_date"
|
|
|
+ v-model="rentalForm.start_date"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="end_date">End Date *</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ id="end_date"
|
|
|
+ v-model="rentalForm.end_date"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="daily_price">Daily Price *</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="daily_price"
|
|
|
+ v-model="rentalForm.daily_price"
|
|
|
+ required
|
|
|
+ min="0"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter daily price"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeRentalModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">Add Rental Price</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add Attachment Modal -->
|
|
|
+ <div v-if="showAttachmentModal" class="modal" @click.self="closeAttachmentModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeAttachmentModal">×</span>
|
|
|
+ <h2>Add Attachment</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveAttachment">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="file_type">Document Type *</label>
|
|
|
+ <select id="file_type" v-model="attachmentForm.file_type" required>
|
|
|
+ <option value="">Select type</option>
|
|
|
+ <option value="receipt">Receipt</option>
|
|
|
+ <option value="warranty">Warranty</option>
|
|
|
+ <option value="other">Other</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="attachment_file">File *</label>
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ id="attachment_file"
|
|
|
+ @change="handleAttachmentUpload"
|
|
|
+ required
|
|
|
+ accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png,.gif"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeAttachmentModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">Add Attachment</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Accounting Section -->
|
|
|
+ <div v-if="activeSection === 'accounting'" class="card">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
+ <h2>Accounting</h2>
|
|
|
+ <div style="display: flex; gap: 10px;">
|
|
|
+ <button class="btn btn-primary" @click="showAddInvoiceModal">Create Invoice</button>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ v-model="invoiceSearch"
|
|
|
+ placeholder="Search invoices..."
|
|
|
+ style="padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
|
|
|
+ @input="searchInvoices"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="invoicesLoading" class="loading">
|
|
|
+ Loading invoices...
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="invoices.length === 0" class="loading">
|
|
|
+ No invoices found. Create your first invoice!
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table v-else class="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>Invoice #</th>
|
|
|
+ <th>Client</th>
|
|
|
+ <th>Issue Date</th>
|
|
|
+ <th>Due Date</th>
|
|
|
+ <th>Status</th>
|
|
|
+ <th>Total</th>
|
|
|
+ <th>Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="invoice in invoices" :key="invoice.id">
|
|
|
+ <td>{{ invoice.invoice_number }}</td>
|
|
|
+ <td>{{ invoice.client_name }}</td>
|
|
|
+ <td>{{ formatDate(invoice.issue_date) }}</td>
|
|
|
+ <td>{{ formatDate(invoice.due_date) }}</td>
|
|
|
+ <td v-html="invoice.getStatusBadge()"></td>
|
|
|
+ <td>${{ parseFloat(invoice.total_amount).toFixed(2) }}</td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button class="btn btn-primary" @click="viewInvoice(invoice)">View</button>
|
|
|
+ <button class="btn btn-warning" @click="editInvoice(invoice)">Edit</button>
|
|
|
+ <button class="btn btn-danger" @click="deleteInvoice(invoice.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add/Edit Client Modal -->
|
|
|
+ <div v-if="showClientModal" class="modal" @click.self="closeClientModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeClientModal">×</span>
|
|
|
+ <h2>{{ isEditingClient ? 'Edit Client' : 'Add New Client' }}</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveClient">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="y_tunnus">Y-Tunnus</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="y_tunnus"
|
|
|
+ v-model="clientForm.y_tunnus"
|
|
|
+ placeholder="Enter Y-tunnus"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="company_name">Company Name</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="company_name"
|
|
|
+ v-model="clientForm.company_name"
|
|
|
+ placeholder="Enter company name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="first_name">First Name *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="first_name"
|
|
|
+ v-model="clientForm.first_name"
|
|
|
+ required
|
|
|
+ placeholder="Enter first name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="last_name">Last Name *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="last_name"
|
|
|
+ v-model="clientForm.last_name"
|
|
|
+ required
|
|
|
+ placeholder="Enter last name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="email">Email *</label>
|
|
|
+ <input
|
|
|
+ type="email"
|
|
|
+ id="email"
|
|
|
+ v-model="clientForm.email"
|
|
|
+ required
|
|
|
+ placeholder="Enter email address"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="phone">Phone</label>
|
|
|
+ <input
|
|
|
+ type="tel"
|
|
|
+ id="phone"
|
|
|
+ v-model="clientForm.phone"
|
|
|
+ placeholder="Enter phone number"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="address">Address</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="address"
|
|
|
+ v-model="clientForm.address"
|
|
|
+ placeholder="Enter street address"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="city">City</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="city"
|
|
|
+ v-model="clientForm.city"
|
|
|
+ placeholder="Enter city"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="state">State/Province</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="state"
|
|
|
+ v-model="clientForm.state"
|
|
|
+ placeholder="Enter state or province"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="postal_code">Postal Code</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="postal_code"
|
|
|
+ v-model="clientForm.postal_code"
|
|
|
+ placeholder="Enter postal code"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="country">Country</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="country"
|
|
|
+ v-model="clientForm.country"
|
|
|
+ placeholder="Enter country"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="notes">Notes</label>
|
|
|
+ <textarea
|
|
|
+ id="notes"
|
|
|
+ v-model="clientForm.notes"
|
|
|
+ placeholder="Enter any additional notes"
|
|
|
+ rows="3"
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeClientModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">
|
|
|
+ {{ isEditingClient ? 'Update' : 'Add' }} Client
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add/Edit Contact Person Modal -->
|
|
|
+ <div v-if="showContactPersonModal" class="modal" @click.self="closeContactPersonModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeContactPersonModal">×</span>
|
|
|
+ <h2>{{ isEditingContactPerson ? 'Edit Contact Person' : 'Add Contact Person' }}</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveContactPerson">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="contact_first_name">First Name *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="contact_first_name"
|
|
|
+ v-model="contactPersonForm.first_name"
|
|
|
+ required
|
|
|
+ placeholder="Enter first name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="contact_last_name">Last Name *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="contact_last_name"
|
|
|
+ v-model="contactPersonForm.last_name"
|
|
|
+ required
|
|
|
+ placeholder="Enter last name"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="contact_email">Email</label>
|
|
|
+ <input
|
|
|
+ type="email"
|
|
|
+ id="contact_email"
|
|
|
+ v-model="contactPersonForm.email"
|
|
|
+ placeholder="Enter email address"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="contact_phone">Phone</label>
|
|
|
+ <input
|
|
|
+ type="tel"
|
|
|
+ id="contact_phone"
|
|
|
+ v-model="contactPersonForm.phone"
|
|
|
+ placeholder="Enter phone number"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="contact_position">Position</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="contact_position"
|
|
|
+ v-model="contactPersonForm.position"
|
|
|
+ placeholder="Enter position/title"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ id="contact_is_primary"
|
|
|
+ v-model="contactPersonForm.is_primary"
|
|
|
+ >
|
|
|
+ Primary Contact
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeContactPersonModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">
|
|
|
+ {{ isEditingContactPerson ? 'Update' : 'Add' }} Contact Person
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Invoice Items Modal -->
|
|
|
+ <div v-if="showInvoiceItemsModal" class="modal" @click.self="closeInvoiceItemsModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <span class="close" @click="closeInvoiceItemsModal">×</span>
|
|
|
+ <h2>{{ invoiceItemsForm.id ? 'Edit Invoice Item' : 'Add Invoice Item' }}</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveInvoiceItem">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="item_id">Item *</label>
|
|
|
+ <select id="item_id" v-model="invoiceItemsForm.item_id" required>
|
|
|
+ <option value="">Select item</option>
|
|
|
+ <option v-for="item in items" :key="item.id" :value="item.id">{{ item.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="item_description">Description</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="item_description"
|
|
|
+ v-model="invoiceItemsForm.description"
|
|
|
+ placeholder="Enter item description"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="item_quantity">Quantity *</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="item_quantity"
|
|
|
+ v-model="invoiceItemsForm.quantity"
|
|
|
+ required
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter quantity"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="item_unit_price">Unit Price *</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="item_unit_price"
|
|
|
+ v-model="invoiceItemsForm.unit_price"
|
|
|
+ required
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter unit price"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="item_line_total">Line Total</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="item_line_total"
|
|
|
+ v-model="invoiceItemsForm.line_total"
|
|
|
+ required
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter line total"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeInvoiceItemsModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">
|
|
|
+ {{ invoiceItemsForm.id ? 'Update' : 'Add' }} Item
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Invoice Details Modal -->
|
|
|
+ <div v-if="showInvoiceModal" class="modal" @click.self="closeInvoiceModal">
|
|
|
+ <div class="modal-content" style="max-width: 900px;">
|
|
|
+ <span class="close" @click="closeInvoiceModal">×</span>
|
|
|
+ <h2>{{ isEditingInvoice ? 'Edit Invoice' : 'Create Invoice' }}</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="saveInvoice">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_client_id">Client *</label>
|
|
|
+ <select id="invoice_client_id" v-model="invoiceForm.client_id" required>
|
|
|
+ <option value="">Select client</option>
|
|
|
+ <option v-for="client in clients" :key="client.id" :value="client.id">{{ client.first_name }} {{ client.last_name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_number">Invoice Number *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="invoice_number"
|
|
|
+ v-model="invoiceForm.invoice_number"
|
|
|
+ required
|
|
|
+ placeholder="Enter invoice number"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="issue_date">Issue Date *</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ id="issue_date"
|
|
|
+ v-model="invoiceForm.issue_date"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="due_date">Due Date *</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ id="due_date"
|
|
|
+ v-model="invoiceForm.due_date"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_status">Status</label>
|
|
|
+ <select id="invoice_status" v-model="invoiceForm.status">
|
|
|
+ <option value="draft">Draft</option>
|
|
|
+ <option value="sent">Sent</option>
|
|
|
+ <option value="paid">Paid</option>
|
|
|
+ <option value="overdue">Overdue</option>
|
|
|
+ <option value="cancelled">Cancelled</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_subtotal">Subtotal</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="invoice_subtotal"
|
|
|
+ v-model="invoiceForm.subtotal"
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter subtotal"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_tax_amount">Tax Amount</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="invoice_tax_amount"
|
|
|
+ v-model="invoiceForm.tax_amount"
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter tax amount"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_total_amount">Total Amount</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="invoice_total_amount"
|
|
|
+ v-model="invoiceForm.total_amount"
|
|
|
+ min="0.01"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="Enter total amount"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="invoice_notes">Notes</label>
|
|
|
+ <textarea
|
|
|
+ id="invoice_notes"
|
|
|
+ v-model="invoiceForm.notes"
|
|
|
+ placeholder="Enter any notes"
|
|
|
+ rows="3"
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
|
|
+ <button type="button" class="btn" @click="closeInvoiceModal">Cancel</button>
|
|
|
+ <button type="submit" class="btn btn-success">
|
|
|
+ {{ isEditingInvoice ? 'Update' : 'Create' }} Invoice
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <!-- Invoice Items -->
|
|
|
+ <div v-if="selectedInvoice.id" style="margin-top: 20px;">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
+ <h3>Invoice Items</h3>
|
|
|
+ <button class="btn btn-primary" @click="showAddInvoiceItemModal">Add Item</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="invoiceItems.length === 0" class="loading">
|
|
|
+ No items found for this invoice.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table v-else class="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>Item</th>
|
|
|
+ <th>Description</th>
|
|
|
+ <th>Quantity</th>
|
|
|
+ <th>Unit Price</th>
|
|
|
+ <th>Line Total</th>
|
|
|
+ <th>Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="item in invoiceItems" :key="item.id">
|
|
|
+ <td>{{ item.item_name }}</td>
|
|
|
+ <td>{{ item.description }}</td>
|
|
|
+ <td>{{ item.quantity }}</td>
|
|
|
+ <td>${{ parseFloat(item.unit_price).toFixed(2) }}</td>
|
|
|
+ <td>${{ parseFloat(item.line_total).toFixed(2) }}</td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button class="btn btn-warning" @click="editInvoiceItem(item)">Edit</button>
|
|
|
+ <button class="btn btn-danger" @click="deleteInvoiceItem(item.id)">Delete</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'App',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ activeSection: 'items',
|
|
|
+ items: [],
|
|
|
+ loading: false,
|
|
|
+ showModal: false,
|
|
|
+ isEditing: false,
|
|
|
+ message: '',
|
|
|
+ messageType: '',
|
|
|
+ formData: {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ description: '',
|
|
|
+ serial_number: '',
|
|
|
+ picture: '',
|
|
|
+ quantity: 0,
|
|
|
+ price: 0
|
|
|
+ },
|
|
|
+ showDetailsModal: false,
|
|
|
+ selectedItem: {},
|
|
|
+ activeTab: 'info',
|
|
|
+ rentalPrices: [],
|
|
|
+ attachments: [],
|
|
|
+ showRentalModal: false,
|
|
|
+ rentalForm: {
|
|
|
+ item_id: null,
|
|
|
+ client_id: null,
|
|
|
+ start_date: '',
|
|
|
+ end_date: '',
|
|
|
+ daily_price: 0
|
|
|
+ },
|
|
|
+ showAttachmentModal: false,
|
|
|
+ attachmentForm: {
|
|
|
+ item_id: null,
|
|
|
+ file_type: '',
|
|
|
+ file: null
|
|
|
+ },
|
|
|
+ clients: [],
|
|
|
+ clientsLoading: false,
|
|
|
+ clientSearch: '',
|
|
|
+ showClientModal: false,
|
|
|
+ isEditingClient: false,
|
|
|
+ clientForm: {
|
|
|
+ id: null,
|
|
|
+ y_tunnus: '',
|
|
|
+ company_name: '',
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ address: '',
|
|
|
+ city: '',
|
|
|
+ state: '',
|
|
|
+ postal_code: '',
|
|
|
+ country: '',
|
|
|
+ notes: ''
|
|
|
+ },
|
|
|
+ contactPersons: [],
|
|
|
+ contactPersonsLoading: false,
|
|
|
+ contactPersonForm: {
|
|
|
+ id: null,
|
|
|
+ client_id: null,
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ position: '',
|
|
|
+ is_primary: false
|
|
|
+ },
|
|
|
+ showContactPersonModal: false,
|
|
|
+ isEditingContactPerson: false
|
|
|
+ },
|
|
|
+ // Authentication data
|
|
|
+ isLoggedIn: false,
|
|
|
+ currentUser: null,
|
|
|
+ loginForm: {
|
|
|
+ username: '',
|
|
|
+ password: ''
|
|
|
+ },
|
|
|
+ loginError: '',
|
|
|
+ invoices: [],
|
|
|
+ invoicesLoading: false,
|
|
|
+ invoiceSearch: '',
|
|
|
+ showInvoiceModal: false,
|
|
|
+ isEditingInvoice: false,
|
|
|
+ selectedInvoice: {},
|
|
|
+ invoiceForm: {
|
|
|
+ id: null,
|
|
|
+ client_id: null,
|
|
|
+ invoice_number: '',
|
|
|
+ issue_date: '',
|
|
|
+ due_date: '',
|
|
|
+ status: 'draft',
|
|
|
+ subtotal: 0,
|
|
|
+ tax_amount: 0,
|
|
|
+ total_amount: 0,
|
|
|
+ notes: ''
|
|
|
+ },
|
|
|
+ showInvoiceItemsModal: false,
|
|
|
+ invoiceItems: [],
|
|
|
+ invoiceItemsForm: {
|
|
|
+ id: null,
|
|
|
+ invoice_id: null,
|
|
|
+ item_id: null,
|
|
|
+ description: '',
|
|
|
+ quantity: 1,
|
|
|
+ unit_price: 0,
|
|
|
+ line_total: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.checkAuthStatus()
|
|
|
+ this.fetchItems()
|
|
|
+ this.fetchClients()
|
|
|
+ this.fetchInvoices()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async fetchItems() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get('/api/items.php')
|
|
|
+ this.items = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching items: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveItem() {
|
|
|
+ try {
|
|
|
+ if (this.isEditing) {
|
|
|
+ await axios.put('/api/items.php', this.formData)
|
|
|
+ this.showMessage('Item updated successfully!', 'success')
|
|
|
+ } else {
|
|
|
+ await axios.post('/api/items.php', this.formData)
|
|
|
+ this.showMessage('Item added successfully!', 'success')
|
|
|
+ }
|
|
|
+
|
|
|
+ this.closeModal()
|
|
|
+ this.fetchItems()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving item: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteItem(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this item?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/items.php?id=${id}`)
|
|
|
+ this.showMessage('Item deleted successfully!', 'success')
|
|
|
+ this.fetchItems()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting item: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddModal() {
|
|
|
+ this.isEditing = false
|
|
|
+ this.formData = {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ description: '',
|
|
|
+ serial_number: '',
|
|
|
+ picture: '',
|
|
|
+ quantity: 0,
|
|
|
+ price: 0
|
|
|
+ }
|
|
|
+ this.showModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ editItem(item) {
|
|
|
+ this.isEditing = true
|
|
|
+ this.formData = {
|
|
|
+ id: item.id,
|
|
|
+ name: item.name,
|
|
|
+ description: item.description || '',
|
|
|
+ serial_number: item.serial_number || '',
|
|
|
+ picture: item.picture || '',
|
|
|
+ quantity: item.quantity,
|
|
|
+ price: item.price
|
|
|
+ }
|
|
|
+ this.showModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeModal() {
|
|
|
+ this.showModal = false
|
|
|
+ this.formData = {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ description: '',
|
|
|
+ serial_number: '',
|
|
|
+ picture: '',
|
|
|
+ quantity: 0,
|
|
|
+ price: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async handleFileUpload(event) {
|
|
|
+ const file = event.target.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('picture', file)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post('/api/upload.php', formData, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.formData.picture = response.data.url
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error uploading picture: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showMessage(text, type) {
|
|
|
+ this.message = text
|
|
|
+ this.messageType = type
|
|
|
+ setTimeout(() => {
|
|
|
+ this.message = ''
|
|
|
+ this.messageType = ''
|
|
|
+ }, 5000)
|
|
|
+ },
|
|
|
+
|
|
|
+ formatDate(dateString) {
|
|
|
+ return new Date(dateString).toLocaleDateString()
|
|
|
+ },
|
|
|
+
|
|
|
+ async viewItemDetails(item) {
|
|
|
+ this.selectedItem = item
|
|
|
+ this.activeTab = 'info'
|
|
|
+ this.showDetailsModal = true
|
|
|
+
|
|
|
+ if (this.activeSection === 'items') {
|
|
|
+ await Promise.all([
|
|
|
+ this.fetchRentalPrices(item.id),
|
|
|
+ this.fetchAttachments(item.id)
|
|
|
+ ])
|
|
|
+ } else if (this.activeSection === 'clients') {
|
|
|
+ await this.fetchContactPersons(item.id)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ closeDetailsModal() {
|
|
|
+ this.showDetailsModal = false
|
|
|
+ this.selectedItem = {}
|
|
|
+ this.rentalPrices = []
|
|
|
+ this.attachments = []
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchRentalPrices(itemId) {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/rental_prices.php?item_id=${itemId}`)
|
|
|
+ this.rentalPrices = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching rental prices: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchAttachments(itemId) {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/attachments.php?item_id=${itemId}`)
|
|
|
+ this.attachments = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching attachments: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddRentalModal() {
|
|
|
+ this.rentalForm = {
|
|
|
+ item_id: this.selectedItem.id,
|
|
|
+ client_id: null,
|
|
|
+ start_date: '',
|
|
|
+ end_date: '',
|
|
|
+ daily_price: 0
|
|
|
+ }
|
|
|
+ this.showRentalModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeRentalModal() {
|
|
|
+ this.showRentalModal = false
|
|
|
+ this.rentalForm = {
|
|
|
+ item_id: null,
|
|
|
+ client_id: null,
|
|
|
+ start_date: '',
|
|
|
+ end_date: '',
|
|
|
+ daily_price: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveRentalPrice() {
|
|
|
+ try {
|
|
|
+ await axios.post('/api/rental_prices.php', this.rentalForm)
|
|
|
+ this.showMessage('Rental price added successfully!', 'success')
|
|
|
+ this.closeRentalModal()
|
|
|
+ await this.fetchRentalPrices(this.selectedItem.id)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving rental price: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteRentalPrice(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this rental price?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/rental_prices.php?id=${id}`)
|
|
|
+ this.showMessage('Rental price deleted successfully!', 'success')
|
|
|
+ await this.fetchRentalPrices(this.selectedItem.id)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting rental price: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddAttachmentModal() {
|
|
|
+ this.attachmentForm = {
|
|
|
+ item_id: this.selectedItem.id,
|
|
|
+ file_type: '',
|
|
|
+ file: null
|
|
|
+ }
|
|
|
+ this.showAttachmentModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeAttachmentModal() {
|
|
|
+ this.showAttachmentModal = false
|
|
|
+ this.attachmentForm = {
|
|
|
+ item_id: null,
|
|
|
+ file_type: '',
|
|
|
+ file: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleAttachmentUpload(event) {
|
|
|
+ this.attachmentForm.file = event.target.files[0]
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveAttachment() {
|
|
|
+ if (!this.attachmentForm.file) {
|
|
|
+ this.showMessage('Please select a file to upload', 'error')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('attachment', this.attachmentForm.file)
|
|
|
+ formData.append('item_id', this.attachmentForm.item_id)
|
|
|
+ formData.append('file_type', this.attachmentForm.file_type)
|
|
|
+
|
|
|
+ await axios.post('/api/attachments.php', formData, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.showMessage('Attachment uploaded successfully!', 'success')
|
|
|
+ this.closeAttachmentModal()
|
|
|
+ await this.fetchAttachments(this.selectedItem.id)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error uploading attachment: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteAttachment(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this attachment?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/attachments.php?id=${id}`)
|
|
|
+ this.showMessage('Attachment deleted successfully!', 'success')
|
|
|
+ await this.fetchAttachments(this.selectedItem.id)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting attachment: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ formatFileSize(bytes) {
|
|
|
+ if (bytes === 0) return '0 Bytes'
|
|
|
+ const k = 1024
|
|
|
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
|
+ },
|
|
|
+
|
|
|
+ getClientName(clientId) {
|
|
|
+ if (!clientId) return 'Unassigned'
|
|
|
+ const client = this.clients.find(c => c.id === clientId)
|
|
|
+ if (!client) return 'Unknown Client'
|
|
|
+ return client.company_name ? `${client.company_name} - ${client.first_name} ${client.last_name}` : `${client.first_name} ${client.last_name}`
|
|
|
+ },
|
|
|
+
|
|
|
+ // Client management methods
|
|
|
+ async fetchClients() {
|
|
|
+ this.clientsLoading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get('/api/clients.php')
|
|
|
+ this.clients = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching clients: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.clientsLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async searchClients() {
|
|
|
+ if (this.clientSearch.trim() === '') {
|
|
|
+ await this.fetchClients()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.clientsLoading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/clients.php?search=${this.clientSearch}`)
|
|
|
+ this.clients = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error searching clients: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.clientsLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddClientModal() {
|
|
|
+ this.isEditingClient = false
|
|
|
+ this.clientForm = {
|
|
|
+ id: null,
|
|
|
+ y_tunnus: '',
|
|
|
+ company_name: '',
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ address: '',
|
|
|
+ city: '',
|
|
|
+ state: '',
|
|
|
+ postal_code: '',
|
|
|
+ country: '',
|
|
|
+ notes: ''
|
|
|
+ }
|
|
|
+ this.showClientModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ editClient(client) {
|
|
|
+ this.isEditingClient = true
|
|
|
+ this.clientForm = {
|
|
|
+ id: client.id,
|
|
|
+ y_tunnus: client.y_tunnus || '',
|
|
|
+ company_name: client.company_name || '',
|
|
|
+ first_name: client.first_name,
|
|
|
+ last_name: client.last_name,
|
|
|
+ email: client.email,
|
|
|
+ phone: client.phone || '',
|
|
|
+ address: client.address || '',
|
|
|
+ city: client.city || '',
|
|
|
+ state: client.state || '',
|
|
|
+ postal_code: client.postal_code || '',
|
|
|
+ country: client.country || '',
|
|
|
+ notes: client.notes || ''
|
|
|
+ }
|
|
|
+ this.showClientModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeClientModal() {
|
|
|
+ this.showClientModal = false
|
|
|
+ this.clientForm = {
|
|
|
+ id: null,
|
|
|
+ y_tunnus: '',
|
|
|
+ company_name: '',
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ address: '',
|
|
|
+ city: '',
|
|
|
+ state: '',
|
|
|
+ postal_code: '',
|
|
|
+ country: '',
|
|
|
+ notes: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveClient() {
|
|
|
+ try {
|
|
|
+ if (this.isEditingClient) {
|
|
|
+ await axios.put('/api/clients.php', this.clientForm)
|
|
|
+ this.showMessage('Client updated successfully!', 'success')
|
|
|
+ } else {
|
|
|
+ await axios.post('/api/clients.php', this.clientForm)
|
|
|
+ this.showMessage('Client added successfully!', 'success')
|
|
|
+ }
|
|
|
+
|
|
|
+ this.closeClientModal()
|
|
|
+ await this.fetchClients()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving client: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteClient(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this client?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/clients.php?id=${id}`)
|
|
|
+ this.showMessage('Client deleted successfully!', 'success')
|
|
|
+ await this.fetchClients()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting client: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Contact Persons management methods
|
|
|
+ async fetchContactPersons(clientId) {
|
|
|
+ this.contactPersonsLoading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/contact_persons.php?client_id=${clientId}`)
|
|
|
+ this.contactPersons = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching contact persons: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.contactPersonsLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddContactPersonModal() {
|
|
|
+ this.isEditingContactPerson = false
|
|
|
+ this.contactPersonForm = {
|
|
|
+ id: null,
|
|
|
+ client_id: this.selectedClient ? this.selectedClient.id : null,
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ position: '',
|
|
|
+ is_primary: false
|
|
|
+ }
|
|
|
+ this.showContactPersonModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ editContactPerson(person) {
|
|
|
+ this.isEditingContactPerson = true
|
|
|
+ this.contactPersonForm = {
|
|
|
+ id: person.id,
|
|
|
+ client_id: person.client_id,
|
|
|
+ first_name: person.first_name,
|
|
|
+ last_name: person.last_name,
|
|
|
+ email: person.email || '',
|
|
|
+ phone: person.phone || '',
|
|
|
+ position: person.position || '',
|
|
|
+ is_primary: person.is_primary
|
|
|
+ }
|
|
|
+ this.showContactPersonModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeContactPersonModal() {
|
|
|
+ this.showContactPersonModal = false
|
|
|
+ this.contactPersonForm = {
|
|
|
+ id: null,
|
|
|
+ client_id: null,
|
|
|
+ first_name: '',
|
|
|
+ last_name: '',
|
|
|
+ email: '',
|
|
|
+ phone: '',
|
|
|
+ position: '',
|
|
|
+ is_primary: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveContactPerson() {
|
|
|
+ try {
|
|
|
+ if (this.isEditingContactPerson) {
|
|
|
+ await axios.put('/api/contact_persons.php', this.contactPersonForm)
|
|
|
+ this.showMessage('Contact person updated successfully!', 'success')
|
|
|
+ } else {
|
|
|
+ await axios.post('/api/contact_persons.php', this.contactPersonForm)
|
|
|
+ this.showMessage('Contact person added successfully!', 'success')
|
|
|
+ }
|
|
|
+
|
|
|
+ this.closeContactPersonModal()
|
|
|
+ await this.fetchContactPersons(this.contactPersonForm.client_id)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving contact person: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteContactPerson(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this contact person?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/contact_persons.php?id=${id}`)
|
|
|
+ this.showMessage('Contact person deleted successfully!', 'success')
|
|
|
+ await this.fetchContactPersons(this.selectedClient ? this.selectedClient.id : null)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting contact person: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Invoice management methods
|
|
|
+ async fetchInvoices() {
|
|
|
+ this.invoicesLoading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get('/api/invoices.php')
|
|
|
+ this.invoices = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error fetching invoices: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.invoicesLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async searchInvoices() {
|
|
|
+ if (this.invoiceSearch.trim() === '') {
|
|
|
+ await this.fetchInvoices()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.invoicesLoading = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/invoices.php?search=${this.invoiceSearch}`)
|
|
|
+ this.invoices = response.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error searching invoices: ' + error.message, 'error')
|
|
|
+ } finally {
|
|
|
+ this.invoicesLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ showAddInvoiceModal() {
|
|
|
+ this.isEditingInvoice = false
|
|
|
+ this.invoiceForm = {
|
|
|
+ id: null,
|
|
|
+ client_id: null,
|
|
|
+ invoice_number: '',
|
|
|
+ issue_date: '',
|
|
|
+ due_date: '',
|
|
|
+ status: 'draft',
|
|
|
+ subtotal: 0,
|
|
|
+ tax_amount: 0,
|
|
|
+ total_amount: 0,
|
|
|
+ notes: ''
|
|
|
+ }
|
|
|
+ this.showInvoiceModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ editInvoice(invoice) {
|
|
|
+ this.isEditingInvoice = true
|
|
|
+ this.invoiceForm = {
|
|
|
+ id: invoice.id,
|
|
|
+ client_id: invoice.client_id,
|
|
|
+ invoice_number: invoice.invoice_number,
|
|
|
+ issue_date: invoice.issue_date,
|
|
|
+ due_date: invoice.due_date,
|
|
|
+ status: invoice.status,
|
|
|
+ subtotal: invoice.subtotal,
|
|
|
+ tax_amount: invoice.tax_amount,
|
|
|
+ total_amount: invoice.total_amount,
|
|
|
+ notes: invoice.notes
|
|
|
+ }
|
|
|
+ this.showInvoiceModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeInvoiceModal() {
|
|
|
+ this.showInvoiceModal = false
|
|
|
+ this.invoiceForm = {
|
|
|
+ id: null,
|
|
|
+ client_id: null,
|
|
|
+ invoice_number: '',
|
|
|
+ issue_date: '',
|
|
|
+ due_date: '',
|
|
|
+ status: 'draft',
|
|
|
+ subtotal: 0,
|
|
|
+ tax_amount: 0,
|
|
|
+ total_amount: 0,
|
|
|
+ notes: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async saveInvoice() {
|
|
|
+ try {
|
|
|
+ if (this.isEditingInvoice) {
|
|
|
+ await axios.put('/api/invoices.php', this.invoiceForm)
|
|
|
+ this.showMessage('Invoice updated successfully!', 'success')
|
|
|
+ } else {
|
|
|
+ await axios.post('/api/invoices.php', this.invoiceForm)
|
|
|
+ this.showMessage('Invoice created successfully!', 'success')
|
|
|
+ }
|
|
|
+
|
|
|
+ this.closeInvoiceModal()
|
|
|
+ await this.fetchInvoices()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving invoice: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteInvoice(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this invoice?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/invoices.php?id=${id}`)
|
|
|
+ this.showMessage('Invoice deleted successfully!', 'success')
|
|
|
+ await this.fetchInvoices()
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting invoice: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ viewInvoice(invoice) {
|
|
|
+ this.selectedInvoice = invoice
|
|
|
+ this.showInvoiceItemsModal = true
|
|
|
+ },
|
|
|
+
|
|
|
+ closeInvoiceItemsModal() {
|
|
|
+ this.showInvoiceItemsModal = false
|
|
|
+ this.selectedInvoice = {}
|
|
|
+ this.invoiceItems = []
|
|
|
+ }
|
|
|
+
|
|
|
+ async saveInvoiceItem() {
|
|
|
+ try {
|
|
|
+ if (this.invoiceItemsForm.id) {
|
|
|
+ await axios.put('/api/invoice_items.php', this.invoiceItemsForm)
|
|
|
+ this.showMessage('Invoice item updated successfully!', 'success')
|
|
|
+ } else {
|
|
|
+ await axios.post('/api/invoice_items.php', this.invoiceItemsForm)
|
|
|
+ this.showMessage('Invoice item added successfully!', 'success')
|
|
|
+ }
|
|
|
+
|
|
|
+ this.closeInvoiceItemsModal()
|
|
|
+ await this.viewInvoice(this.selectedInvoice)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error saving invoice item: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async deleteInvoiceItem(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this invoice item?')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await axios.delete(`/api/invoice_items.php?id=${id}`)
|
|
|
+ this.showMessage('Invoice item deleted successfully!', 'success')
|
|
|
+ await this.viewInvoice(this.selectedInvoice)
|
|
|
+ } catch (error) {
|
|
|
+ this.showMessage('Error deleting invoice item: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Authentication methods
|
|
|
+ async login() {
|
|
|
+ this.loginError = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post('/api/auth.php', {
|
|
|
+ action: 'login',
|
|
|
+ username: this.loginForm.username,
|
|
|
+ password: this.loginForm.password
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.user) {
|
|
|
+ this.isLoggedIn = true
|
|
|
+ this.currentUser = response.data.user
|
|
|
+ this.loginForm = { username: '', password: '' }
|
|
|
+ this.showMessage('Login successful!', 'success')
|
|
|
+ this.$refs.loginUsername.value = ''
|
|
|
+ this.$refs.loginPassword.value = ''
|
|
|
+ } else {
|
|
|
+ this.loginError = response.data.message || 'Login failed'
|
|
|
+ this.showMessage('Login failed', 'error')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.loginError = 'Network error. Please try again.'
|
|
|
+ this.showMessage('Error during login: ' + error.message, 'error')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ logout() {
|
|
|
+ this.isLoggedIn = false
|
|
|
+ this.currentUser = null
|
|
|
+ this.showMessage('Logged out successfully!', 'success')
|
|
|
+ },
|
|
|
+
|
|
|
+ checkAuthStatus() {
|
|
|
+ try {
|
|
|
+ const response = await axios.get('/api/auth.php?action=status')
|
|
|
+ if (response.data.user) {
|
|
|
+ this.isLoggedIn = true
|
|
|
+ this.currentUser = response.data.user
|
|
|
+ } else {
|
|
|
+ this.isLoggedIn = false
|
|
|
+ this.currentUser = null
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.isLoggedIn = false
|
|
|
+ this.currentUser = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.alert {
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.alert.error {
|
|
|
+ background-color: #fdf2f2;
|
|
|
+ color: #e74c3c;
|
|
|
+ border: 1px solid #f5c6cb;
|
|
|
+}
|
|
|
+
|
|
|
+.alert.success {
|
|
|
+ background-color: #f2f9f4;
|
|
|
+ color: #27ae60;
|
|
|
+ border: 1px solid #c3e6cb;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-tabs {
|
|
|
+ display: flex;
|
|
|
+ border-bottom: 2px solid #e9ecef;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-tab {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ padding: 12px 24px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
+ transition: all 0.3s;
|
|
|
+ color: #6c757d;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-tab:hover {
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ color: #495057;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-tab.active {
|
|
|
+ border-bottom-color: #3498db;
|
|
|
+ color: #3498db;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs {
|
|
|
+ display: flex;
|
|
|
+ border-bottom: 2px solid #e9ecef;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ padding: 12px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-btn:hover {
|
|
|
+ background-color: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-btn.active {
|
|
|
+ border-bottom-color: #3498db;
|
|
|
+ color: #3498db;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-content {
|
|
|
+ min-height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.item-info {
|
|
|
+ display: grid;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-row {
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.attachments-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 15px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-type {
|
|
|
+ background-color: #6c757d;
|
|
|
+ color: white;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ font-weight: 500;
|
|
|
+ width: fit-content;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-size {
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-actions .btn {
|
|
|
+ text-decoration: none;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.contacts-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-person-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 15px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-person-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-position {
|
|
|
+ background-color: #6c757d;
|
|
|
+ color: white;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ font-weight: 500;
|
|
|
+ width: fit-content;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-primary {
|
|
|
+ background-color: #28a745;
|
|
|
+ color: white;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ width: fit-content;
|
|
|
+ margin-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-person-details {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 5px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-person-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Login Styles */
|
|
|
+.login-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.login-card {
|
|
|
+ background: white;
|
|
|
+ padding: 2rem;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
+ width: 100%;
|
|
|
+ max-width: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.app-header {
|
|
|
+ background: white;
|
|
|
+ padding: 1rem 2rem;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.user-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.user-role {
|
|
|
+ background: #6c757d;
|
|
|
+ color: white;
|
|
|
+ padding: 0.25rem 0.5rem;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ font-weight: 500;
|
|
|
+ text-transform: uppercase;
|
|
|
+}
|
|
|
+
|
|
|
+.main-content {
|
|
|
+ padding-top: 2rem;
|
|
|
+}
|
|
|
+</style>
|