Files
star-erp/resources/js/Pages/Landlord/Tenant/Show.tsx
sky121113 3fd333085b
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 57s
feat: 實作 POS API 整合功能,包含商品與銷售訂單同步及韌性機制
2026-02-06 11:56:29 +08:00

305 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import LandlordLayout from "@/Layouts/LandlordLayout";
import { Link, useForm, router } from "@inertiajs/react";
import { Globe, Plus, Trash2, ArrowLeft } from "lucide-react";
import { FormEvent, useState } from "react";
interface Domain {
id: number;
domain: string;
}
interface Tenant {
id: string;
name: string;
short_name: string | null;
email: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
domains: Domain[];
}
interface Token {
id: number;
name: string;
last_used_at: string;
created_at: string;
}
interface Flash {
success: string | null;
error: string | null;
new_token: string | null;
}
interface Props {
tenant: Tenant;
tokens: Token[];
flash: Flash;
}
export default function TenantShow({ tenant, tokens = [], flash }: Props) {
const [showAddDomain, setShowAddDomain] = useState(false);
const [showAddToken, setShowAddToken] = useState(false);
const { data, setData, post, processing, errors, reset } = useForm({
domain: "",
});
// Token Form
const {
data: tokenData,
setData: setTokenData,
post: postToken,
processing: processingToken,
reset: resetToken
} = useForm({
name: "",
});
const handleAddDomain = (e: FormEvent) => {
e.preventDefault();
post(route("landlord.tenants.domains.store", tenant.id), {
onSuccess: () => {
reset();
setShowAddDomain(false);
},
});
};
const handleAddToken = (e: FormEvent) => {
e.preventDefault();
postToken(route("landlord.tenants.tokens.store", tenant.id), {
onSuccess: () => {
resetToken();
// Don't close immediately if we want to show flash message?
// Flash message usually appears on redirect back.
setShowAddToken(false);
},
});
};
const handleRevokeToken = (tokenId: number) => {
if (confirm("確定要撤銷此金鑰嗎撤銷後無法復原POS 連線將中斷。")) {
router.delete(route("landlord.tenants.tokens.destroy", [tenant.id, tokenId]));
}
};
const handleRemoveDomain = (domainId: number) => {
if (confirm("確定要移除這個域名嗎?")) {
router.delete(route("landlord.tenants.domains.destroy", [tenant.id, domainId]));
}
};
return (
<LandlordLayout
title="客戶詳情"
>
<div className="max-w-3xl space-y-6">
{/* Back Link */}
<Link
href="/landlord/tenants"
className="inline-flex items-center gap-1 text-slate-600 hover:text-slate-900"
>
<ArrowLeft className="w-4 h-4" />
</Link>
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">{tenant.name}</h1>
<p className="text-slate-500 mt-1"> ID: {tenant.id}</p>
</div>
<div className="flex gap-2">
<Link
href={`/landlord/tenants/${tenant.id}/branding`}
className="bg-primary-main hover:bg-primary-dark text-white px-4 py-2 rounded-lg transition-colors"
>
</Link>
<Link
href={`/landlord/tenants/${tenant.id}/edit`}
className="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg transition-colors"
>
</Link>
</div>
</div>
{/* Info Card */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4"></h2>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-slate-500"></dt>
<dd className="mt-1">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${tenant.is_active
? "bg-green-100 text-green-700"
: "bg-slate-100 text-slate-600"
}`}
>
{tenant.is_active ? "啟用" : "停用"}
</span>
</dd>
</div>
<div>
<dt className="text-sm text-slate-500"></dt>
<dd className="mt-1 text-slate-900">{tenant.short_name || "-"}</dd>
</div>
<div>
<dt className="text-sm text-slate-500"></dt>
<dd className="mt-1 text-slate-900">{tenant.email || "-"}</dd>
</div>
<div>
<dt className="text-sm text-slate-500"></dt>
<dd className="mt-1 text-slate-900">{tenant.created_at}</dd>
</div>
<div>
<dt className="text-sm text-slate-500"></dt>
<dd className="mt-1 text-slate-900">{tenant.updated_at}</dd>
</div>
</dl>
</div>
{/* Domains Card */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900"></h2>
<button
onClick={() => setShowAddDomain(!showAddDomain)}
className="text-primary-main hover:text-primary-dark flex items-center gap-1 text-sm"
>
<Plus className="w-4 h-4" />
</button>
</div>
{showAddDomain && (
<form onSubmit={handleAddDomain} className="mb-4 flex gap-2">
<input
type="text"
value={data.domain}
onChange={(e) => setData("domain", e.target.value)}
placeholder="例如koori.erp.koori.tw"
className="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
<button
type="submit"
disabled={processing}
className="bg-primary-main hover:bg-primary-dark text-white px-4 py-2 rounded-lg disabled:opacity-50"
>
</button>
</form>
)}
{errors.domain && <p className="mb-4 text-sm text-red-500">{errors.domain}</p>}
{tenant.domains.length === 0 ? (
<p className="text-slate-500 text-sm"></p>
) : (
<ul className="space-y-2">
{tenant.domains.map((domain) => (
<li
key={domain.id}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg"
>
<div className="flex items-center gap-2">
<Globe className="w-4 h-4 text-slate-400" />
<span className="text-slate-900">{domain.domain}</span>
</div>
<button
onClick={() => handleRemoveDomain(domain.id)}
className="p-1 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</li>
))}
</ul>
)}
</div>
{/* API Tokens Card */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">API (POS )</h2>
<button
onClick={() => setShowAddToken(!showAddToken)}
className="text-primary-main hover:text-primary-dark flex items-center gap-1 text-sm"
>
<Plus className="w-4 h-4" />
</button>
</div>
{flash?.new_token && (
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-sm text-green-800 font-bold mb-1"></p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-white px-2 py-1 rounded border border-green-200 text-sm break-all font-mono select-all">
{flash.new_token}
</code>
</div>
</div>
)}
{showAddToken && (
<form onSubmit={handleAddToken} className="mb-4 flex gap-2">
<input
type="text"
value={tokenData.name}
onChange={(e) => setTokenData("name", e.target.value)}
placeholder="金鑰名稱 (例如: POS-01)"
className="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
<button
type="submit"
disabled={processingToken}
className="bg-primary-main hover:bg-primary-dark text-white px-4 py-2 rounded-lg disabled:opacity-50"
>
</button>
</form>
)}
{tokens.length === 0 ? (
<p className="text-slate-500 text-sm"> API </p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-slate-500 uppercase bg-slate-50">
<tr>
<th className="px-3 py-2"></th>
<th className="px-3 py-2"></th>
<th className="px-3 py-2">使</th>
<th className="px-3 py-2 text-right"></th>
</tr>
</thead>
<tbody>
{tokens.map((token) => (
<tr key={token.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50">
<td className="px-3 py-2 font-medium text-slate-900">{token.name}</td>
<td className="px-3 py-2 text-slate-500">{token.created_at}</td>
<td className="px-3 py-2 text-slate-500">{token.last_used_at}</td>
<td className="px-3 py-2 text-right">
<button
onClick={() => handleRevokeToken(token.id)}
className="text-red-600 hover:text-red-900 hover:underline"
>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</LandlordLayout>
);
}