Admin panel updated with shadcn-vue components
This commit is contained in:
@@ -1,10 +1,42 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import { Head, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPaperPlane, faCoins, faTags, faFlask } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
MessageSquareIcon,
|
||||
PlusIcon,
|
||||
SendIcon,
|
||||
CoinsIcon,
|
||||
TagIcon,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
initialProfiles: { type: Array, default: () => [] },
|
||||
@@ -85,13 +117,17 @@ async function submitTest() {
|
||||
delivery_report: !!testForm.delivery_report,
|
||||
country_code: testForm.country_code,
|
||||
};
|
||||
await router.post(route("admin.sms-profiles.test-send", testTarget.value.id), payload, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
testOpen.value = false;
|
||||
},
|
||||
});
|
||||
await router.post(
|
||||
route("admin.sms-profiles.test-send", testTarget.value.id),
|
||||
payload,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
testOpen.value = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
testForm.processing = false;
|
||||
}
|
||||
@@ -119,175 +155,188 @@ const formatDateTime = (s) => (s ? new Date(s).toLocaleString() : "—");
|
||||
<template>
|
||||
<AdminLayout title="SMS profili">
|
||||
<Head title="SMS profili" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
|
||||
SMS profili
|
||||
<span class="text-xs font-medium text-gray-400">({{ profiles.length }})</span>
|
||||
</h1>
|
||||
<button
|
||||
@click="openCreate"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nov profil
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Ime</th>
|
||||
<th class="px-3 py-2 text-left">Uporabnik</th>
|
||||
<th class="px-3 py-2">Aktiven</th>
|
||||
<th class="px-3 py-2">Pošiljatelji</th>
|
||||
<th class="px-3 py-2">Bilanca</th>
|
||||
<th class="px-3 py-2">Cena</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in profiles" :key="p.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ p.name }}</td>
|
||||
<td class="px-3 py-2">{{ p.api_username }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="p.active ? 'text-emerald-600' : 'text-rose-600'">{{ p.active ? 'Da' : 'Ne' }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-600">
|
||||
<span v-if="(p.senders||[]).length === 0">—</span>
|
||||
<span v-else>
|
||||
{{ p.senders.map(s => s.sname).join(', ') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchBalance(p)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-700 border-amber-300 bg-amber-50 hover:bg-amber-100">
|
||||
<FontAwesomeIcon :icon="faCoins" class="w-3.5 h-3.5" /> Pridobi
|
||||
</button>
|
||||
<span class="text-xs text-gray-600">{{ balances[p.id] ?? '—' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchPrice(p)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-700 border-indigo-300 bg-indigo-50 hover:bg-indigo-100">
|
||||
<FontAwesomeIcon :icon="faTags" class="w-3.5 h-3.5" /> Cene
|
||||
</button>
|
||||
<span class="text-xs text-gray-600 truncate max-w-[200px]" :title="(quotes[p.id]||[]).join(', ')">
|
||||
{{ (quotes[p.id] || []).join(', ') || '—' }}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<MessageSquareIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>
|
||||
SMS profili
|
||||
<Badge variant="secondary" class="ml-2">{{ profiles.length }}</Badge>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button @click="openCreate">
|
||||
<PlusIcon class="h-4 w-4 mr-2" />
|
||||
Nov profil
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Ime</TableHead>
|
||||
<TableHead>Uporabnik</TableHead>
|
||||
<TableHead>Aktiven</TableHead>
|
||||
<TableHead>Pošiljatelji</TableHead>
|
||||
<TableHead>Bilanca</TableHead>
|
||||
<TableHead>Cena</TableHead>
|
||||
<TableHead class="text-right">Akcije</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="p in profiles" :key="p.id">
|
||||
<TableCell class="font-medium">{{ p.name }}</TableCell>
|
||||
<TableCell>{{ p.api_username }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="p.active ? 'default' : 'secondary'">
|
||||
{{ p.active ? "Da" : "Ne" }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground text-xs">
|
||||
<span v-if="(p.senders || []).length === 0">—</span>
|
||||
<span v-else>
|
||||
{{ p.senders.map((s) => s.sname).join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<button
|
||||
@click="openTest(p)"
|
||||
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-emerald-700 border-emerald-300 bg-emerald-50 hover:bg-emerald-100"
|
||||
title="Pošlji test SMS"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Test SMS
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="fetchBalance(p)" variant="outline" size="sm">
|
||||
<CoinsIcon class="h-3.5 w-3.5 mr-1" /> Pridobi
|
||||
</Button>
|
||||
<span class="text-xs text-muted-foreground">{{
|
||||
balances[p.id] ?? "—"
|
||||
}}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="fetchPrice(p)" variant="outline" size="sm">
|
||||
<TagIcon class="h-3.5 w-3.5 mr-1" /> Cene
|
||||
</Button>
|
||||
<span
|
||||
class="text-xs text-muted-foreground truncate max-w-[200px]"
|
||||
:title="(quotes[p.id] || []).join(', ')"
|
||||
>
|
||||
{{ (quotes[p.id] || []).join(", ") || "—" }}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button
|
||||
@click="openTest(p)"
|
||||
variant="default"
|
||||
size="sm"
|
||||
title="Pošlji test SMS"
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5 mr-1" /> Test SMS
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Create Profile Modal -->
|
||||
<CreateDialog
|
||||
:show="createOpen"
|
||||
title="Nov SMS profil"
|
||||
max-width="2xl"
|
||||
confirm-text="Shrani"
|
||||
:processing="createForm.processing"
|
||||
@close="() => (createOpen = false)"
|
||||
@confirm="submitCreate"
|
||||
>
|
||||
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-5">
|
||||
<Dialog :open="createOpen" @update:open="(val) => (createOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nov SMS profil</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="createForm.name" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label>Ime</Label>
|
||||
<Input v-model="createForm.name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="createForm.active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Aktivno</Label>
|
||||
<Select v-model="createForm.active">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API uporabnik</label>
|
||||
<input v-model="createForm.api_username" type="text" class="input" />
|
||||
<div class="space-y-2">
|
||||
<Label>API uporabnik</Label>
|
||||
<Input v-model="createForm.api_username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API geslo</label>
|
||||
<input v-model="createForm.api_password" type="password" class="input" autocomplete="new-password" />
|
||||
<div class="space-y-2">
|
||||
<Label>API geslo</Label>
|
||||
<Input
|
||||
v-model="createForm.api_password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CreateDialog>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="() => (createOpen = false)"
|
||||
>Prekliči</Button
|
||||
>
|
||||
<Button
|
||||
form="create-sms-profile"
|
||||
type="submit"
|
||||
:disabled="createForm.processing"
|
||||
>Shrani</Button
|
||||
>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Test Send Modal -->
|
||||
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
|
||||
<template #title> Testni SMS </template>
|
||||
<template #content>
|
||||
<Dialog :open="testOpen" @update:open="(val) => (testOpen = val)">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Testni SMS</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input v-model="testForm.to" type="text" class="input" placeholder="+386..." />
|
||||
<div class="space-y-2">
|
||||
<Label>Prejemnik (E.164)</Label>
|
||||
<Input v-model="testForm.to" type="text" placeholder="+386..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda (opcijsko)</label>
|
||||
<input v-model="testForm.country_code" type="text" class="input" placeholder="SI" />
|
||||
<div class="space-y-2">
|
||||
<Label>Državna koda (opcijsko)</Label>
|
||||
<Input v-model="testForm.country_code" type="text" placeholder="SI" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="label">Sporočilo</label>
|
||||
<textarea v-model="testForm.message" class="input" rows="4"></textarea>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label>Sporočilo</Label>
|
||||
<Textarea v-model="testForm.message" rows="4" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Dostavna poročila</Label>
|
||||
<Select v-model="testForm.delivery_report">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="true">Da</SelectItem>
|
||||
<SelectItem :value="false">Ne</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result details removed; rely on flash message after redirect -->
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" @click="() => (testOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Zapri</button>
|
||||
<button type="button" @click="submitTest" :disabled="testForm.processing || !testTarget" class="px-4 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50">
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5 mr-1" /> Pošlji test
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="() => (testOpen = false)"
|
||||
>Zapri</Button
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !testTarget"
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5 mr-2" /> Pošlji test
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--tw-color-gray-300, #d1d5db);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user