@@ -0,0 +1,104 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type Order = {
|
||||
id: number;
|
||||
uuid: string;
|
||||
plan_code: string;
|
||||
gateway: string;
|
||||
status: string;
|
||||
total: number;
|
||||
currency: string;
|
||||
created_at: string;
|
||||
tenant: { name: string };
|
||||
};
|
||||
|
||||
export default function BillingOrders({
|
||||
orders,
|
||||
}: {
|
||||
orders: { data: Order[] };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Order Lisensi" />
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 md:p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Order Lisensi</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Verifikasi pembayaran manual dan pantau checkout seluruh
|
||||
tenant.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-xl border">
|
||||
<table className="w-full min-w-[760px] text-sm">
|
||||
<thead className="bg-muted/50 text-left">
|
||||
<tr>
|
||||
{[
|
||||
'Tenant',
|
||||
'Paket',
|
||||
'Gateway',
|
||||
'Total',
|
||||
'Status',
|
||||
'Dibuat',
|
||||
'Aksi',
|
||||
].map((item) => (
|
||||
<th key={item} className="px-4 py-3">
|
||||
{item}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.data.map((order) => (
|
||||
<tr key={order.uuid} className="border-t">
|
||||
<td className="px-4 py-3 font-medium">
|
||||
{order.tenant.name}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{order.plan_code}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{order.gateway}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: order.currency,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(order.total)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{order.status}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{new Date(
|
||||
order.created_at,
|
||||
).toLocaleString('id-ID')}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{order.status === 'pending' && (
|
||||
<Form
|
||||
action={`/administration/billing-orders/${order.id}/approve`}
|
||||
method="patch"
|
||||
>
|
||||
<Button size="sm">
|
||||
Konfirmasi Bayar
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
BillingOrders.layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Order Lisensi', href: '/administration/billing-orders' },
|
||||
],
|
||||
};
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
export default function TenantCreate() {
|
||||
export default function TenantCreate({
|
||||
plans,
|
||||
}: {
|
||||
plans: Record<
|
||||
string,
|
||||
{ name: string; price: number; max_devices: number; max_users: number }
|
||||
>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Tambah Tenant" />
|
||||
@@ -158,6 +165,34 @@ export default function TenantCreate() {
|
||||
message={errors.deployment_domain}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label>Paket Lisensi</Label>
|
||||
<select
|
||||
name="plan_code"
|
||||
defaultValue="cloud_free"
|
||||
className="h-10 rounded-md border bg-background px-3"
|
||||
>
|
||||
{Object.entries(plans).map(
|
||||
([code, plan]) => (
|
||||
<option
|
||||
key={code}
|
||||
value={code}
|
||||
>
|
||||
{plan.name} ·{' '}
|
||||
{plan.price === 0
|
||||
? 'Gratis'
|
||||
: `Rp${plan.price.toLocaleString('id-ID')}/bulan`}{' '}
|
||||
· {plan.max_devices}{' '}
|
||||
perangkat ·{' '}
|
||||
{plan.max_users} user
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
<InputError
|
||||
message={errors.plan_code}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-end gap-3">
|
||||
|
||||
@@ -33,15 +33,29 @@ type Installation = {
|
||||
status: string;
|
||||
last_seen_at?: string;
|
||||
};
|
||||
type License = {
|
||||
plan: string;
|
||||
status: string;
|
||||
max_devices: number;
|
||||
max_users: number;
|
||||
expires_at: string;
|
||||
};
|
||||
|
||||
export default function TenantEdit({
|
||||
tenant,
|
||||
users,
|
||||
installation,
|
||||
license,
|
||||
plans,
|
||||
}: {
|
||||
tenant: Tenant;
|
||||
users: User[];
|
||||
installation: Installation | null;
|
||||
license: License | null;
|
||||
plans: Record<
|
||||
string,
|
||||
{ name: string; price: number; max_devices: number; max_users: number }
|
||||
>;
|
||||
}) {
|
||||
const { flash } = usePage().props as unknown as {
|
||||
flash: { success?: string };
|
||||
@@ -150,6 +164,37 @@ export default function TenantEdit({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label>Paket Lisensi Aktif</Label>
|
||||
<select
|
||||
name="plan_code"
|
||||
defaultValue={
|
||||
license?.plan ?? 'cloud_free'
|
||||
}
|
||||
className="h-10 rounded-md border bg-background px-3"
|
||||
>
|
||||
{Object.entries(plans).map(
|
||||
([code, plan]) => (
|
||||
<option key={code} value={code}>
|
||||
{plan.name} ·{' '}
|
||||
{plan.price === 0
|
||||
? 'Gratis'
|
||||
: `Rp${plan.price.toLocaleString('id-ID')}/bulan`}{' '}
|
||||
· {plan.max_devices}{' '}
|
||||
perangkat · {plan.max_users}{' '}
|
||||
user
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Status: {license?.status ?? 'belum ada'}
|
||||
{license?.expires_at
|
||||
? ` · sampai ${new Date(license.expires_at).toLocaleDateString('id-ID')}`
|
||||
: ''}
|
||||
</p>
|
||||
<InputError message={errors.plan_code} />
|
||||
</div>
|
||||
<div className="flex gap-3 md:col-span-2">
|
||||
<Button disabled={processing}>
|
||||
Simpan Perubahan
|
||||
|
||||
@@ -17,6 +17,7 @@ type Tenant = {
|
||||
domain?: string;
|
||||
status: string;
|
||||
}[];
|
||||
licenses: { plan: string; status: string; expires_at: string }[];
|
||||
};
|
||||
type PageLink = { url: string | null; label: string; active: boolean };
|
||||
type Props = { tenants: { data: Tenant[]; links: PageLink[]; total: number } };
|
||||
@@ -62,6 +63,7 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
<th className="py-3">Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Instalasi</th>
|
||||
<th>Paket</th>
|
||||
<th>Users</th>
|
||||
<th>Dibuat</th>
|
||||
<th className="text-right">Aksi</th>
|
||||
@@ -94,6 +96,16 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
: 'Nonaktif'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<div className="font-medium">
|
||||
{tenant.licenses[0]?.plan ??
|
||||
'-'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tenant.licenses[0]?.status ??
|
||||
'belum ada'}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="font-medium">
|
||||
{tenant.installations[0]
|
||||
@@ -139,7 +151,7 @@ export default function TenantIndex({ tenants }: Props) {
|
||||
{tenants.data.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
colSpan={7}
|
||||
className="py-12 text-center text-muted-foreground"
|
||||
>
|
||||
Belum ada tenant. Buat tenant
|
||||
|
||||
@@ -8,8 +8,9 @@ import { Label } from '@/components/ui/label';
|
||||
|
||||
type Props = {
|
||||
tenant: { id: number; name: string };
|
||||
canManageRoles: boolean;
|
||||
};
|
||||
export default function UserCreate({ tenant }: Props) {
|
||||
export default function UserCreate({ tenant, canManageRoles }: Props) {
|
||||
const base = `/administration/tenants/${tenant.id}/users`;
|
||||
|
||||
return (
|
||||
@@ -57,10 +58,28 @@ export default function UserCreate({ tenant }: Props) {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-muted/40 px-4 py-3 text-sm md:col-span-2">
|
||||
Akun baru otomatis menggunakan role baku{' '}
|
||||
<strong>Tenant User</strong>.
|
||||
</div>
|
||||
{canManageRoles ? (
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label>Role</Label>
|
||||
<select
|
||||
name="role"
|
||||
defaultValue="TENANT USER"
|
||||
className="h-10 rounded-md border bg-background px-3"
|
||||
>
|
||||
<option value="TENANT ADMIN">
|
||||
Tenant Admin
|
||||
</option>
|
||||
<option value="TENANT USER">
|
||||
Tenant User
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border bg-muted/40 px-4 py-3 text-sm md:col-span-2">
|
||||
Akun baru otomatis menggunakan role baku{' '}
|
||||
<strong>Tenant User</strong>.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 md:col-span-2">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={base}>Batal</Link>
|
||||
|
||||
@@ -16,8 +16,13 @@ type Role = { id: number; name: string };
|
||||
type Props = {
|
||||
tenant: { id: number; name: string };
|
||||
managedUser: { id: number; name: string; email: string; roles: Role[] };
|
||||
canManageRoles: boolean;
|
||||
};
|
||||
export default function UserEdit({ tenant, managedUser }: Props) {
|
||||
export default function UserEdit({
|
||||
tenant,
|
||||
managedUser,
|
||||
canManageRoles,
|
||||
}: Props) {
|
||||
const base = `/administration/tenants/${tenant.id}/users`;
|
||||
const { flash } = usePage().props as unknown as {
|
||||
flash: { success?: string };
|
||||
@@ -65,14 +70,36 @@ export default function UserEdit({ tenant, managedUser }: Props) {
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-muted/40 px-4 py-3 text-sm md:col-span-2">
|
||||
Role sistem:{' '}
|
||||
<strong>
|
||||
{managedUser.roles[0]?.name ??
|
||||
'Tenant User'}
|
||||
</strong>
|
||||
. Role bersifat baku dan tidak dapat diubah.
|
||||
</div>
|
||||
{canManageRoles ? (
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label>Role</Label>
|
||||
<select
|
||||
name="role"
|
||||
defaultValue={
|
||||
managedUser.roles[0]?.name ??
|
||||
'TENANT USER'
|
||||
}
|
||||
className="h-10 rounded-md border bg-background px-3"
|
||||
>
|
||||
<option value="TENANT ADMIN">
|
||||
Tenant Admin
|
||||
</option>
|
||||
<option value="TENANT USER">
|
||||
Tenant User
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border bg-muted/40 px-4 py-3 text-sm md:col-span-2">
|
||||
Role sistem:{' '}
|
||||
<strong>
|
||||
{managedUser.roles[0]?.name ??
|
||||
'Tenant User'}
|
||||
</strong>
|
||||
. Role bersifat baku dan tidak dapat
|
||||
diubah.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 md:col-span-2">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={base}>Kembali</Link>
|
||||
|
||||
Reference in New Issue
Block a user