ifixkart-admin/app/(admin)/settings/page.tsx

756 lines
33 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import {
Settings,
Save,
Loader2,
User,
Shield,
FileText,
Building,
RefreshCw,
Upload,
Image as ImageIcon,
KeyRound,
LayoutDashboard,
Search,
Copy,
Check,
Lock,
} from 'lucide-react';
import { parseJwt, getAccessToken } from '@/services/api/client';
import { userService, UserResponse } from '@/services/api/userService';
import { toast } from 'sonner';
import { format } from 'date-fns';
type SettingsBlade = 'overview' | 'profile' | 'security' | 'branding';
function PropertyRow({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-[118px_minmax(0,1fr)] gap-x-3 gap-y-1 py-3 border-b border-border items-start sm:items-center min-w-0">
<dt className="text-[13px] text-muted-foreground leading-5 truncate">{label}</dt>
<dd className="text-[13px] font-semibold text-foreground leading-5 min-w-0 overflow-hidden">
{typeof children === 'string' ? (
<span className="block truncate" title={children}>
{children}
</span>
) : (
children
)}
</dd>
</div>
);
}
export default function SettingsPage() {
const [userRole, setUserRole] = useState('');
const [blade, setBlade] = useState<SettingsBlade>('overview');
const [navSearch, setNavSearch] = useState('');
const [copiedId, setCopiedId] = useState(false);
const [profile, setProfile] = useState<UserResponse | null>(null);
const [loadingProfile, setLoadingProfile] = useState(true);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [userName, setUserName] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [savingProfile, setSavingProfile] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [savingPassword, setSavingPassword] = useState(false);
const [invoiceLogoUrl, setInvoiceLogoUrl] = useState('https://ifixkart.com/logo.png');
const [invoiceCompanyName, setInvoiceCompanyName] = useState('iFixKart Solutions Platform');
const [invoiceGstin, setInvoiceGstin] = useState('33AAAAA0000A1Z5');
const [invoiceStoreAddress, setInvoiceStoreAddress] = useState(
'Offline Main Store Counter, Chennai | +91 9876543210'
);
const [invoiceGstRate, setInvoiceGstRate] = useState('18.0');
const [uploadingLogo, setUploadingLogo] = useState(false);
const isSuperAdmin =
userRole === 'super admin' || userRole === 'super_admin' || userRole === 'superadmin';
const fetchBrandingSettings = async () => {
if (typeof window === 'undefined') return;
const savedBranding = localStorage.getItem('ifixkart_invoice_branding');
if (savedBranding) {
try {
const parsed = JSON.parse(savedBranding);
if (parsed.logoUrl) setInvoiceLogoUrl(parsed.logoUrl);
if (parsed.companyName) setInvoiceCompanyName(parsed.companyName);
if (parsed.gstin) setInvoiceGstin(parsed.gstin);
if (parsed.storeAddress) setInvoiceStoreAddress(parsed.storeAddress);
if (parsed.gstRate !== undefined) setInvoiceGstRate(String(parsed.gstRate));
} catch {
/* ignore corrupt local branding */
}
}
try {
const token = getAccessToken();
const backendUrl = '';
const res = await fetch(`${backendUrl}/api/v1/settings/key/invoice_branding`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
const val = data.setting_value;
if (val && typeof val === 'object') {
if (val.logoUrl) setInvoiceLogoUrl(val.logoUrl);
if (val.companyName) setInvoiceCompanyName(val.companyName);
if (val.gstin) setInvoiceGstin(val.gstin);
if (val.storeAddress) setInvoiceStoreAddress(val.storeAddress);
if (val.gstRate !== undefined) setInvoiceGstRate(String(val.gstRate));
}
}
} catch {
/* branding endpoint optional */
}
};
const fetchUserProfile = async () => {
setLoadingProfile(true);
try {
const token = getAccessToken();
if (!token) return;
const payload = parseJwt(token);
const userId = payload?.sub as string;
if (!userId) return;
const user = await userService.getProfile(userId);
setProfile(user);
setFirstName(user.first_name || '');
setLastName(user.last_name || '');
setPhone(user.phone || '');
setEmail(user.email || '');
setUserName(user.display_name || user.email.split('@')[0]);
} catch {
toast.error('Failed to load user profile');
} finally {
setLoadingProfile(false);
}
};
useEffect(() => {
const token = getAccessToken();
if (token) {
const payload = parseJwt(token);
if (payload) {
setUserRole(((payload.role as string) || '').toLowerCase());
}
}
fetchBrandingSettings();
fetchUserProfile();
}, []);
useEffect(() => {
if (blade === 'branding' && !isSuperAdmin) setBlade('overview');
}, [blade, isSuperAdmin]);
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault();
if (!profile) return;
setSavingProfile(true);
try {
await userService.update(profile.user_id, {
first_name: firstName,
last_name: lastName,
phone: phone,
display_name: userName,
});
toast.success('Profile saved');
fetchUserProfile();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to update profile';
toast.error(message);
} finally {
setSavingProfile(false);
}
};
const handleDiscardProfile = () => {
if (!profile) return;
setFirstName(profile.first_name || '');
setLastName(profile.last_name || '');
setPhone(profile.phone || '');
setEmail(profile.email || '');
setUserName(profile.display_name || profile.email.split('@')[0]);
};
const handleUpdatePassword = async (e: React.FormEvent) => {
e.preventDefault();
if (!newPassword || !confirmPassword) {
toast.error('Enter a new password');
return;
}
if (newPassword !== confirmPassword) {
toast.error('New password and confirmation do not match');
return;
}
setSavingPassword(true);
try {
if (!profile) return;
await userService.update(profile.user_id, {
password: newPassword,
});
toast.success('Password updated');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Password update failed';
toast.error(message);
} finally {
setSavingPassword(false);
}
};
const handleLogoFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingLogo(true);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('entity_type', 'invoice_branding');
formData.append('entity_id', 'logo');
const token = getAccessToken();
const backendUrl = '';
const res = await fetch(`${backendUrl}/api/v1/files/upload`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
});
if (!res.ok) {
const reader = new FileReader();
reader.onload = (evt) => {
if (evt.target?.result) {
setInvoiceLogoUrl(evt.target.result as string);
toast.success('Invoice logo uploaded');
}
};
reader.readAsDataURL(file);
return;
}
const data = await res.json();
const relativePath =
data.webp_path || data.storage_path || data.thumbnail_path || data.raw_path || data.url || data.file_path || '';
const uploadedUrl = relativePath.startsWith('http')
? relativePath
: `${backendUrl}${relativePath.startsWith('/') ? '' : '/'}${relativePath}`;
setInvoiceLogoUrl(uploadedUrl);
toast.success('Invoice logo uploaded');
} catch {
const reader = new FileReader();
reader.onload = (evt) => {
if (evt.target?.result) {
setInvoiceLogoUrl(evt.target.result as string);
toast.success('Invoice logo loaded');
}
};
reader.readAsDataURL(file);
} finally {
setUploadingLogo(false);
}
};
const handleSaveInvoiceBranding = async () => {
const brandingData = {
logoUrl: invoiceLogoUrl,
companyName: invoiceCompanyName,
gstin: invoiceGstin,
storeAddress: invoiceStoreAddress,
gstRate: Number(invoiceGstRate) || 18.0,
};
localStorage.setItem('ifixkart_invoice_branding', JSON.stringify(brandingData));
try {
const token = getAccessToken();
const backendUrl = '';
await fetch(`${backendUrl}/api/v1/settings/save/invoice_branding`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
setting_value: brandingData,
description: 'Invoice logo and store branding configurations',
is_public: true,
}),
});
} catch {
/* save still persisted locally */
}
toast.success('Invoice logo and branding saved');
};
const copyUserId = async () => {
if (!profile?.user_id) return;
try {
await navigator.clipboard.writeText(profile.user_id);
setCopiedId(true);
toast.success('User ID copied to clipboard');
setTimeout(() => setCopiedId(false), 1500);
} catch {
toast.error('Could not copy the user ID');
}
};
const reloadAll = () => {
fetchUserProfile();
fetchBrandingSettings();
};
const navItems = useMemo(
() =>
[
{ id: 'overview' as const, label: 'Overview', hint: 'Resource essentials', icon: LayoutDashboard },
{ id: 'profile' as const, label: 'Identity', hint: 'Name, phone, username', icon: User },
{ id: 'security' as const, label: 'Access', hint: 'Password credentials', icon: KeyRound },
...(isSuperAdmin
? [{ id: 'branding' as const, label: 'Invoice branding', hint: 'Logo, GSTIN, store', icon: FileText }]
: []),
].filter((item) => {
const q = navSearch.trim().toLowerCase();
if (!q) return true;
return item.label.toLowerCase().includes(q) || item.hint.toLowerCase().includes(q);
}),
[isSuperAdmin, navSearch]
);
const dataCardShell =
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-visible min-w-0';
const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5';
const inputClass =
'w-full h-9 px-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors';
const primaryButton =
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 whitespace-nowrap';
const secondaryButton =
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50 whitespace-nowrap';
const fullName = [firstName, lastName].filter(Boolean).join(' ') || profile?.email || 'Administrator';
const roleLabel = userRole ? userRole.replace(/_/g, ' ') : 'Admin';
const bladeMeta: Record<SettingsBlade, { title: string; subtitle: string }> = {
overview: {
title: 'Overview',
subtitle: 'Account resource essentials and current identity status.',
},
profile: {
title: 'Identity',
subtitle: 'Update the display credentials used across the admin console.',
},
security: {
title: 'Access credentials',
subtitle: 'Rotate the password used to sign in to this administrator account.',
},
branding: {
title: 'Invoice branding',
subtitle: 'Logo, legal name, GSTIN and store contact printed on invoices.',
},
};
return (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">System Settings</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none capitalize">
{roleLabel}
</span>
</div>
</div>
<button
type="button"
onClick={reloadAll}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Reload"
>
<RefreshCw className={`w-3.5 h-3.5 ${loadingProfile ? 'animate-spin' : ''}`} />
</button>
</div>
<div className={`${dataCardShell} p-4`}>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 min-w-0">
<div className="flex items-center gap-3 min-w-0">
<div className="w-11 h-11 crm-radius-icon bg-primary/10 text-primary flex items-center justify-center shrink-0">
<Settings className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[15px] font-semibold text-foreground truncate">{fullName}</p>
<p className="text-[13px] text-muted-foreground truncate">
{email || profile?.email || 'Administrator account'} · iFixKart console
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-1.5">
<span
className={`inline-flex items-center px-2 py-0.5 crm-radius-badge text-[11px] font-semibold ${
profile?.is_active !== false ? 'bg-success text-white' : 'bg-muted text-muted-foreground'
}`}
>
{profile?.is_active !== false ? 'Active' : 'Inactive'}
</span>
<span className="inline-flex items-center px-2 py-0.5 crm-radius-badge bg-muted text-foreground text-[11px] font-semibold capitalize">
{roleLabel}
</span>
{profile?.email_verified && (
<span className="inline-flex items-center px-2 py-0.5 crm-radius-badge bg-success/10 text-success text-[11px] font-semibold">
Email verified
</span>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-[240px_minmax(0,1fr)] gap-4 min-w-0 items-start">
<aside className={`${dataCardShell} p-3 xl:sticky xl:top-5`}>
<div className="relative mb-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<input
type="text"
value={navSearch}
onChange={(e) => setNavSearch(e.target.value)}
placeholder="Filter settings"
className="w-full h-9 pl-8 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary"
/>
</div>
<nav className="flex flex-col gap-1">
{navItems.length === 0 ? (
<p className="px-3 py-6 text-center text-[12px] text-muted-foreground">No matching settings.</p>
) : (
navItems.map((item) => {
const Icon = item.icon;
const selected = blade === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => setBlade(item.id)}
className={`relative w-full flex items-center gap-2.5 h-auto min-h-10 px-3 py-2 text-left crm-radius-toggle cursor-pointer transition-colors ${
selected
? 'bg-card border border-[#b8c0d4] text-foreground'
: 'border border-transparent text-foreground hover:bg-muted'
}`}
>
{selected && <span className="absolute left-0 top-1.5 bottom-1.5 w-[3px] bg-primary" />}
<span
className={`w-7 h-7 crm-radius-icon flex items-center justify-center shrink-0 ${
selected ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
}`}
>
<Icon className="w-3.5 h-3.5" />
</span>
<span className="min-w-0 overflow-hidden">
<span className="block text-[13px] font-semibold leading-5 truncate">{item.label}</span>
<span className="block text-[12px] text-muted-foreground mt-0.5 truncate">{item.hint}</span>
</span>
</button>
);
})
)}
</nav>
</aside>
<section className={`${dataCardShell} overflow-hidden`}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 px-5 py-4 border-b border-border min-w-0">
<div className="min-w-0">
<h2 className="text-[16px] font-semibold text-foreground">{bladeMeta[blade].title}</h2>
<p className="text-[13px] text-muted-foreground mt-1">{bladeMeta[blade].subtitle}</p>
</div>
{blade === 'profile' && (
<div className="flex items-center gap-2 shrink-0">
<button type="button" onClick={handleDiscardProfile} className={secondaryButton}>
Discard
</button>
<button type="submit" form="settings-profile-form" disabled={savingProfile} className={primaryButton}>
{savingProfile ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
Save
</button>
</div>
)}
{blade === 'security' && (
<button type="submit" form="settings-security-form" disabled={savingPassword} className={primaryButton}>
{savingPassword ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Lock className="w-3.5 h-3.5" />}
Update password
</button>
)}
{blade === 'branding' && isSuperAdmin && (
<button type="button" onClick={handleSaveInvoiceBranding} className={primaryButton}>
<Save className="w-3.5 h-3.5" />
Save branding
</button>
)}
</div>
<div className="p-5 min-w-0 overflow-hidden">
{loadingProfile && blade !== 'branding' ? (
<div className="py-16 flex justify-center">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : blade === 'overview' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6 min-w-0">
<dl className="min-w-0 overflow-hidden">
<p className="text-[14px] font-semibold text-foreground mb-1">Essentials</p>
<PropertyRow label="Resource">{fullName}</PropertyRow>
<PropertyRow label="User name">{userName || '—'}</PropertyRow>
<PropertyRow label="Email">{email || '—'}</PropertyRow>
<PropertyRow label="Phone">{phone || '—'}</PropertyRow>
<PropertyRow label="Role">
<span className="block truncate capitalize">{roleLabel}</span>
</PropertyRow>
</dl>
<dl className="min-w-0 overflow-hidden">
<p className="text-[14px] font-semibold text-foreground mb-1">Directory</p>
<PropertyRow label="User ID">
<div className="flex items-center gap-2 min-w-0 w-full">
<span className="block min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[13px]" title={profile?.user_id}>
{profile?.user_id || '—'}
</span>
{profile?.user_id && (
<button
type="button"
title="Copy user ID"
onClick={copyUserId}
className="w-7 h-7 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer flex items-center justify-center shrink-0"
>
{copiedId ? <Check className="w-3.5 h-3.5 text-success" /> : <Copy className="w-3.5 h-3.5" />}
</button>
)}
</div>
</PropertyRow>
<PropertyRow label="Employee code">{profile?.employee_code || '—'}</PropertyRow>
<PropertyRow label="Last login">
{profile?.last_login ? format(new Date(profile.last_login), 'dd MMM yyyy, hh:mm a') : '—'}
</PropertyRow>
<PropertyRow label="Created">
{profile?.created_at ? format(new Date(profile.created_at), 'dd MMM yyyy') : '—'}
</PropertyRow>
<PropertyRow label="Status">
<span
className={`inline-flex items-center px-2 py-0.5 crm-radius-badge text-[12px] font-semibold ${
profile?.is_active !== false ? 'bg-success text-white' : 'bg-muted text-muted-foreground'
}`}
>
{profile?.is_active !== false ? 'Active' : 'Inactive'}
</span>
</PropertyRow>
</dl>
<div className="lg:col-span-2 flex flex-wrap gap-2 pt-1">
<button type="button" onClick={() => setBlade('profile')} className={secondaryButton}>
<User className="w-3.5 h-3.5" />
Edit identity
</button>
<button type="button" onClick={() => setBlade('security')} className={secondaryButton}>
<Shield className="w-3.5 h-3.5" />
Manage access
</button>
</div>
</div>
) : blade === 'profile' ? (
<form id="settings-profile-form" onSubmit={handleSaveProfile} className="space-y-4 max-w-3xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className={labelClass}>First name</label>
<input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Last name</label>
<input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Username</label>
<input type="text" value={userName} onChange={(e) => setUserName(e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Phone number</label>
<input type="text" value={phone} onChange={(e) => setPhone(e.target.value)} className={inputClass} />
</div>
<div className="md:col-span-2">
<label className={labelClass}>Email address</label>
<input
type="email"
value={email}
disabled
className={`${inputClass} bg-muted text-muted-foreground cursor-not-allowed`}
/>
<p className="text-[13px] text-muted-foreground mt-1.5">Email is bound to this directory account and cannot be changed here.</p>
</div>
</div>
</form>
) : blade === 'security' ? (
<form id="settings-security-form" onSubmit={handleUpdatePassword} className="space-y-4 max-w-3xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className={labelClass}>Current password</label>
<input
type="password"
placeholder="••••••••"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>New password</label>
<input
type="password"
placeholder="••••••••"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Confirm new password</label>
<input
type="password"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={inputClass}
/>
</div>
</div>
<p className="text-[13px] text-muted-foreground">
Updating the password signs this administrator in with the new credential on the next login.
</p>
</form>
) : (
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_260px] gap-5 min-w-0">
<div className="space-y-4 min-w-0">
<div>
<label className={labelClass}>Company / invoice brand logo</label>
<div className="flex items-start gap-3 min-w-0">
{invoiceLogoUrl ? (
<div className="w-16 h-16 crm-radius-icon border border-border bg-card p-1.5 flex items-center justify-center overflow-hidden shrink-0">
<img src={invoiceLogoUrl} alt="Invoice logo preview" className="max-w-full max-h-full object-contain" />
</div>
) : (
<div className="w-16 h-16 crm-radius-icon border border-dashed border-border bg-muted/20 flex flex-col items-center justify-center shrink-0 text-muted-foreground">
<ImageIcon className="w-5 h-5" />
</div>
)}
<div className="flex-1 min-w-0 space-y-2">
<input
type="text"
value={invoiceLogoUrl}
onChange={(e) => setInvoiceLogoUrl(e.target.value)}
placeholder="https://ifixkart.com/logo.png"
className={inputClass}
/>
<input
type="file"
accept="image/*"
id="invoiceLogoFileInput"
className="hidden"
onChange={handleLogoFileUpload}
/>
<button
type="button"
disabled={uploadingLogo}
onClick={() => document.getElementById('invoiceLogoFileInput')?.click()}
className={secondaryButton}
>
{uploadingLogo ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
{uploadingLogo ? 'Uploading...' : 'Upload logo'}
</button>
<p className="text-[13px] text-muted-foreground">PNG, JPG, WebP or SVG. Transparent logos print cleanest.</p>
</div>
</div>
</div>
<div>
<label className={labelClass}>Company legal name</label>
<input
type="text"
value={invoiceCompanyName}
onChange={(e) => setInvoiceCompanyName(e.target.value)}
placeholder="iFixKart Solutions Platform"
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>GSTIN / tax registration no.</label>
<input
type="text"
value={invoiceGstin}
onChange={(e) => setInvoiceGstin(e.target.value)}
placeholder="33AAAAA0000A1Z5"
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Default GST / Tax Rate (%)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.1"
min="0"
max="100"
value={invoiceGstRate}
onChange={(e) => setInvoiceGstRate(e.target.value)}
placeholder="18.0"
className={inputClass}
/>
<span className="text-[13px] font-semibold text-muted-foreground">%</span>
</div>
<p className="text-[12px] text-muted-foreground mt-1">
Configures default GST split on generated tax invoices (e.g. 18% = CGST {(Number(invoiceGstRate) / 2 || 9)}% + SGST {(Number(invoiceGstRate) / 2 || 9)}%).
</p>
</div>
<div>
<label className={labelClass}>Store address & contact</label>
<input
type="text"
value={invoiceStoreAddress}
onChange={(e) => setInvoiceStoreAddress(e.target.value)}
placeholder="Offline Main Store Counter, Chennai | +91 9876543210"
className={inputClass}
/>
</div>
</div>
<aside className="crm-radius-section border border-border bg-muted/20 p-4 min-w-0 overflow-hidden h-fit">
<p className="text-[13px] font-semibold text-foreground mb-3">Invoice preview</p>
<div className="crm-radius-section border border-border bg-card p-4 min-w-0 overflow-hidden">
<div className="flex items-center gap-2 pb-3 border-b border-border min-w-0">
{invoiceLogoUrl ? (
<img src={invoiceLogoUrl} alt="" className="h-8 w-8 object-contain shrink-0" />
) : (
<Building className="w-5 h-5 text-muted-foreground shrink-0" />
)}
<div className="min-w-0 overflow-hidden">
<p className="text-[13px] font-semibold text-foreground truncate">{invoiceCompanyName || 'Company name'}</p>
<p className="text-[12px] text-muted-foreground truncate">Tax invoice</p>
</div>
</div>
<dl className="mt-3 space-y-2 text-[13px] min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<dt className="text-muted-foreground shrink-0">GSTIN</dt>
<dd className="font-mono font-semibold text-foreground min-w-0 truncate">{invoiceGstin || '—'}</dd>
</div>
<div className="min-w-0">
<dt className="text-muted-foreground">Store</dt>
<dd className="font-medium text-foreground mt-0.5 leading-5 break-words">{invoiceStoreAddress || '—'}</dd>
</div>
</dl>
</div>
</aside>
</div>
)}
</div>
</section>
</div>
</div>
);
}