|
|
@@ -1,22 +1,248 @@
|
|
|
<template>
|
|
|
<div class="page">
|
|
|
<div class="page-header">
|
|
|
- <h1>Organizations</h1>
|
|
|
- <p>Manage all organizations</p>
|
|
|
+ <div>
|
|
|
+ <h1>{{ $t('organizations.title') }}</h1>
|
|
|
+ <p>{{ $t('organizations.manage') }}</p>
|
|
|
+ </div>
|
|
|
+ <button @click="showCreateModal" class="btn-primary">
|
|
|
+ {{ $t('organizations.add') }}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
|
|
|
<div class="content">
|
|
|
- <p>Organizations list will be here...</p>
|
|
|
+ <div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
|
|
+
|
|
|
+ <div v-else-if="error" class="error">{{ error }}</div>
|
|
|
+
|
|
|
+ <table v-else-if="organizations.length > 0" class="data-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>ID</th>
|
|
|
+ <th>{{ $t('organizations.name') }}</th>
|
|
|
+ <th>{{ $t('organizations.contactEmail') }}</th>
|
|
|
+ <th>{{ $t('organizations.contactPhone') }}</th>
|
|
|
+ <th>WiFi</th>
|
|
|
+ <th>BLE</th>
|
|
|
+ <th>{{ $t('common.status') }}</th>
|
|
|
+ <th>{{ $t('common.actions') }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="org in organizations" :key="org.id">
|
|
|
+ <td>{{ org.id }}</td>
|
|
|
+ <td><strong>{{ org.name }}</strong></td>
|
|
|
+ <td>{{ org.contact_email }}</td>
|
|
|
+ <td>{{ org.contact_phone || '-' }}</td>
|
|
|
+ <td><span class="badge" :class="{ active: org.wifi_enabled }">{{ org.wifi_enabled ? 'โ' : 'โ' }}</span></td>
|
|
|
+ <td><span class="badge" :class="{ active: org.ble_enabled }">{{ org.ble_enabled ? 'โ' : 'โ' }}</span></td>
|
|
|
+ <td><span class="badge" :class="`status-${org.status}`">{{ org.status }}</span></td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button @click="showEditModal(org)" class="btn-icon" title="Edit">โ๏ธ</button>
|
|
|
+ <button @click="confirmDelete(org)" class="btn-icon" title="Delete">๐๏ธ</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <div v-else class="empty">No organizations yet</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Create/Edit Modal -->
|
|
|
+ <div v-if="modalVisible" class="modal-overlay" @click="closeModal">
|
|
|
+ <div class="modal" @click.stop>
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>{{ editingOrg ? $t('common.edit') : $t('organizations.add') }}</h2>
|
|
|
+ <button @click="closeModal" class="btn-close">ร</button>
|
|
|
+ </div>
|
|
|
+ <form @submit.prevent="saveOrganization" class="modal-body">
|
|
|
+ <div class="form-group">
|
|
|
+ <label>{{ $t('organizations.name') }} *</label>
|
|
|
+ <input v-model="form.name" type="text" required />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>{{ $t('organizations.contactEmail') }} *</label>
|
|
|
+ <input v-model="form.contact_email" type="email" required />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>{{ $t('organizations.contactPhone') }}</label>
|
|
|
+ <input v-model="form.contact_phone" type="tel" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-row">
|
|
|
+ <div class="form-group">
|
|
|
+ <label class="checkbox">
|
|
|
+ <input v-model="form.wifi_enabled" type="checkbox" />
|
|
|
+ <span>{{ $t('organizations.wifiEnabled') }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label class="checkbox">
|
|
|
+ <input v-model="form.ble_enabled" type="checkbox" />
|
|
|
+ <span>{{ $t('organizations.bleEnabled') }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label>{{ $t('common.status') }}</label>
|
|
|
+ <select v-model="form.status">
|
|
|
+ <option value="pending">Pending</option>
|
|
|
+ <option value="active">Active</option>
|
|
|
+ <option value="suspended">Suspended</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button type="button" @click="closeModal" class="btn-secondary">{{ $t('common.cancel') }}</button>
|
|
|
+ <button type="submit" :disabled="saving" class="btn-primary">
|
|
|
+ {{ saving ? $t('common.loading') : $t('common.save') }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Delete Confirmation Modal -->
|
|
|
+ <div v-if="deleteConfirmVisible" class="modal-overlay" @click="deleteConfirmVisible = false">
|
|
|
+ <div class="modal modal-sm" @click.stop>
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>{{ $t('common.confirm') }}</h2>
|
|
|
+ </div>
|
|
|
+ <div class="modal-body">
|
|
|
+ <p>Delete organization <strong>{{ organizationToDelete?.name }}</strong>?</p>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button @click="deleteConfirmVisible = false" class="btn-secondary">{{ $t('common.cancel') }}</button>
|
|
|
+ <button @click="deleteOrganization" :disabled="deleting" class="btn-danger">
|
|
|
+ {{ deleting ? $t('common.loading') : $t('common.delete') }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import organizationsApi from '@/api/organizations'
|
|
|
+
|
|
|
+const organizations = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const error = ref(null)
|
|
|
+const modalVisible = ref(false)
|
|
|
+const deleteConfirmVisible = ref(false)
|
|
|
+const editingOrg = ref(null)
|
|
|
+const organizationToDelete = ref(null)
|
|
|
+const saving = ref(false)
|
|
|
+const deleting = ref(false)
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ name: '',
|
|
|
+ contact_email: '',
|
|
|
+ contact_phone: '',
|
|
|
+ wifi_enabled: false,
|
|
|
+ ble_enabled: false,
|
|
|
+ status: 'pending'
|
|
|
+})
|
|
|
+
|
|
|
+async function loadOrganizations() {
|
|
|
+ loading.value = true
|
|
|
+ error.value = null
|
|
|
+ try {
|
|
|
+ organizations.value = await organizationsApi.getAll()
|
|
|
+ } catch (err) {
|
|
|
+ error.value = err.response?.data?.detail || 'Failed to load organizations'
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function showCreateModal() {
|
|
|
+ editingOrg.value = null
|
|
|
+ form.value = {
|
|
|
+ name: '',
|
|
|
+ contact_email: '',
|
|
|
+ contact_phone: '',
|
|
|
+ wifi_enabled: false,
|
|
|
+ ble_enabled: false,
|
|
|
+ status: 'pending'
|
|
|
+ }
|
|
|
+ modalVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function showEditModal(org) {
|
|
|
+ editingOrg.value = org
|
|
|
+ form.value = {
|
|
|
+ name: org.name,
|
|
|
+ contact_email: org.contact_email,
|
|
|
+ contact_phone: org.contact_phone || '',
|
|
|
+ wifi_enabled: org.wifi_enabled,
|
|
|
+ ble_enabled: org.ble_enabled,
|
|
|
+ status: org.status
|
|
|
+ }
|
|
|
+ modalVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function closeModal() {
|
|
|
+ modalVisible.value = false
|
|
|
+ editingOrg.value = null
|
|
|
+}
|
|
|
+
|
|
|
+async function saveOrganization() {
|
|
|
+ saving.value = true
|
|
|
+ try {
|
|
|
+ if (editingOrg.value) {
|
|
|
+ await organizationsApi.update(editingOrg.value.id, form.value)
|
|
|
+ } else {
|
|
|
+ await organizationsApi.create(form.value)
|
|
|
+ }
|
|
|
+ await loadOrganizations()
|
|
|
+ closeModal()
|
|
|
+ } catch (err) {
|
|
|
+ alert(err.response?.data?.detail || 'Failed to save organization')
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function confirmDelete(org) {
|
|
|
+ organizationToDelete.value = org
|
|
|
+ deleteConfirmVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteOrganization() {
|
|
|
+ deleting.value = true
|
|
|
+ try {
|
|
|
+ await organizationsApi.delete(organizationToDelete.value.id)
|
|
|
+ await loadOrganizations()
|
|
|
+ deleteConfirmVisible.value = false
|
|
|
+ } catch (err) {
|
|
|
+ alert(err.response?.data?.detail || 'Failed to delete organization')
|
|
|
+ } finally {
|
|
|
+ deleting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadOrganizations()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
<style scoped>
|
|
|
.page {
|
|
|
padding: 32px;
|
|
|
}
|
|
|
|
|
|
.page-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-start;
|
|
|
margin-bottom: 32px;
|
|
|
}
|
|
|
|
|
|
@@ -38,4 +264,258 @@
|
|
|
padding: 24px;
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
+
|
|
|
+.loading, .error, .empty {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px;
|
|
|
+ color: #718096;
|
|
|
+}
|
|
|
+
|
|
|
+.error {
|
|
|
+ color: #e53e3e;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table th {
|
|
|
+ text-align: left;
|
|
|
+ padding: 12px;
|
|
|
+ border-bottom: 2px solid #e2e8f0;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #4a5568;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table td {
|
|
|
+ padding: 12px;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+ color: #1a202c;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table tbody tr:hover {
|
|
|
+ background: #f7fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.badge {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ background: #e2e8f0;
|
|
|
+ color: #718096;
|
|
|
+}
|
|
|
+
|
|
|
+.badge.active {
|
|
|
+ background: #c6f6d5;
|
|
|
+ color: #22543d;
|
|
|
+}
|
|
|
+
|
|
|
+.badge.status-active {
|
|
|
+ background: #c6f6d5;
|
|
|
+ color: #22543d;
|
|
|
+}
|
|
|
+
|
|
|
+.badge.status-pending {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #92400e;
|
|
|
+}
|
|
|
+
|
|
|
+.badge.status-suspended {
|
|
|
+ background: #fed7d7;
|
|
|
+ color: #742a2a;
|
|
|
+}
|
|
|
+
|
|
|
+.actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-icon {
|
|
|
+ padding: 4px 8px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 16px;
|
|
|
+ opacity: 0.7;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-icon:hover {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary {
|
|
|
+ padding: 12px 24px;
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:hover {
|
|
|
+ background: #5568d3;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-secondary {
|
|
|
+ padding: 12px 24px;
|
|
|
+ background: #e2e8f0;
|
|
|
+ color: #4a5568;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-secondary:hover {
|
|
|
+ background: #cbd5e0;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-danger {
|
|
|
+ padding: 12px 24px;
|
|
|
+ background: #f56565;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-danger:hover {
|
|
|
+ background: #e53e3e;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.modal {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 600px;
|
|
|
+ max-height: 90vh;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-sm {
|
|
|
+ max-width: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 24px;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header h2 {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1a202c;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-close {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: none;
|
|
|
+ background: none;
|
|
|
+ font-size: 32px;
|
|
|
+ color: #718096;
|
|
|
+ cursor: pointer;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-close:hover {
|
|
|
+ color: #1a202c;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-body {
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #4a5568;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input[type="text"],
|
|
|
+.form-group input[type="email"],
|
|
|
+.form-group input[type="tel"],
|
|
|
+.form-group select {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input:focus,
|
|
|
+.form-group select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.form-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.checkbox {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.checkbox input[type="checkbox"] {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.checkbox span {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #4a5568;
|
|
|
+}
|
|
|
</style>
|