update lisensi prosess
tests / ci (push) Has been cancelled

This commit is contained in:
Wian Drs
2026-08-25 13:43:55 +07:00
parent 1e80be180e
commit 60946ddf37
58 changed files with 2903 additions and 165 deletions
@@ -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>