Compare commits

...

2 Commits

Author SHA1 Message Date
8885c55817 lat and long 2026-07-27 12:09:41 +05:30
6a6221f9f3 Auto-geocode tenant/store address into latitude/longitude on onboarding
Both the tenant onboarding and store/branch forms only had free-text
address fields with no coordinates captured, even though the backend
already accepts and stores latitude/longitude on both tenants and
tenantlocations. Wires in the existing keyless AddressAutocomplete
(Nominatim) component used by UsersPanel so picking an address also
geocodes it, and threads latitude/longitude through both create payloads.
2026-07-27 11:34:35 +05:30
3 changed files with 70 additions and 105 deletions

View File

@@ -32,6 +32,7 @@ import {
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr, num as fnum } from '../services/fiestaApi';
import StoreQRView from './StoreQRView';
import AddressAutocomplete, { type AddressResult } from './AddressAutocomplete';
export default function AdminConsole({ activeTab: propActiveTab, showHeader = true, onBack, tenantId }: { activeTab?: 'tenant' | 'store' | 'rider', showHeader?: boolean, onBack?: () => void, tenantId?: number }) {
const [activeTab, setActiveTab] = useState<'tenant' | 'store' | 'rider'>(propActiveTab || 'tenant');
@@ -62,6 +63,10 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
city: 'Coimbatore',
state: 'Tamil Nadu',
postcode: '',
// Populated from AddressAutocomplete's geocoding lookup, not typed —
// keeps HQ address and coordinates from drifting apart.
latitude: '',
longitude: '',
// Primary outlet — created in the same call as the tenant, so a fresh
// tenant is never left without a location to actually operate from.
locationname: '',
@@ -84,6 +89,10 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
city: 'Coimbatore',
state: 'Tamil Nadu',
postcode: '',
// Populated from AddressAutocomplete's geocoding lookup, not typed —
// keeps the branch address and coordinates from drifting apart.
latitude: '',
longitude: '',
contactno: '',
email: '',
opentime: '06:00:00',
@@ -128,6 +137,34 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
setTimeout(() => setCopiedSql(false), 2000);
};
// Address autocomplete → discrete fields + geocoded lat/long (or clear them
// when the field is emptied), same pattern UsersPanel uses for team members.
const handleTenantAddressSelect = (r: AddressResult | null) => {
setTenantForm((f) => ({
...f,
address: r?.address ?? '',
suburb: r?.suburb ?? '',
city: r?.city ?? '',
state: r?.state ?? '',
postcode: r?.postcode ?? '',
latitude: r?.latitude ?? '',
longitude: r?.longitude ?? '',
}));
};
const handleStoreAddressSelect = (r: AddressResult | null) => {
setStoreForm((f) => ({
...f,
address: r?.address ?? '',
suburb: r?.suburb ?? '',
city: r?.city ?? '',
state: r?.state ?? '',
postcode: r?.postcode ?? '',
latitude: r?.latitude ?? '',
longitude: r?.longitude ?? '',
}));
};
// ----------------------------------------------------
// Submissions
// ----------------------------------------------------
@@ -154,6 +191,8 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
city: tenantForm.city,
state: tenantForm.state,
postcode: tenantForm.postcode,
latitude: tenantForm.latitude,
longitude: tenantForm.longitude,
applocationid: tenantForm.applocationid,
},
});
@@ -353,13 +392,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">HQ Street Address</label>
<input
type="text"
placeholder="e.g. 12, Avinashi Road"
value={tenantForm.address}
onChange={(e) => setTenantForm({ ...tenantForm, address: e.target.value })}
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
/>
<AddressAutocomplete value={tenantForm.address} onSelect={handleTenantAddressSelect} placeholder="e.g. 12, Avinashi Road" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
@@ -471,7 +504,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
)}
<button
type="button"
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', latitude: '', longitude: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer active:scale-95 transition-all shadow-sm"
>
Add Another Branch
@@ -561,18 +594,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
</h4>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Store Branch Address</label>
<div className="relative rounded-xl">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
<MapPin size={14} />
</div>
<input
type="text"
placeholder="e.g. 240, DB Road"
value={storeForm.address}
onChange={(e) => setStoreForm({ ...storeForm, address: e.target.value })}
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
/>
</div>
<AddressAutocomplete value={storeForm.address} onSelect={handleStoreAddressSelect} placeholder="e.g. 240, DB Road" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
@@ -1107,13 +1129,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">HQ Street Address</label>
<input
type="text"
placeholder="e.g. 12, Avinashi Road"
value={tenantForm.address}
onChange={(e) => setTenantForm({ ...tenantForm, address: e.target.value })}
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
/>
<AddressAutocomplete value={tenantForm.address} onSelect={handleTenantAddressSelect} placeholder="e.g. 12, Avinashi Road" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
@@ -1218,7 +1234,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
</button>
)}
<button
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', latitude: '', longitude: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer"
>
Add Another Branch
@@ -1281,13 +1297,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Store Branch Address</label>
<input
type="text"
placeholder="e.g. 240, DB Road"
value={storeForm.address}
onChange={(e) => setStoreForm({ ...storeForm, address: e.target.value })}
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
/>
<AddressAutocomplete value={storeForm.address} onSelect={handleStoreAddressSelect} placeholder="e.g. 240, DB Road" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">

View File

@@ -132,60 +132,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
return (
<div className="space-y-lg animate-in fade-in duration-500 relative">
{/* ── Immersive Executive Banner (cover image + slate→purple gradient overlay) ── */}
<div className="relative p-6 md:p-8 text-white shadow-xl border border-purple-500/20 overflow-hidden animate-in fade-in duration-300">
{/* Cover image background & decorative glow */}
<div className="absolute inset-0 z-0 overflow-hidden">
<img
src="https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1400&q=80"
alt="Executive operations dashboard"
className="w-full h-full object-cover object-center opacity-40"
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-900/90 to-purple-950/80" />
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none" />
<div className="absolute bottom-0 left-0 w-56 h-56 bg-indigo-500/10 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none" />
</div>
{/* Content row */}
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-lg">
<div>
<h1 className="font-sans font-bold text-2xl md:text-3xl tracking-tight text-white flex items-center gap-2.5">
Executive Command Center
<span className="text-[10px] text-purple-200 font-bold bg-purple-900/60 border border-purple-500/30 px-2 py-0.5 rounded-full uppercase tracking-wider animate-pulse">
Live Core
</span>
</h1>
<p className="text-slate-300 font-sans text-sm mt-2 leading-relaxed">
Month-to-date order operations for <strong className="text-white font-semibold">{tenantName}</strong>, pulled live from the API.
</p>
<div className="mt-4">
{loading ? (
<span className="inline-flex items-center gap-1.5 text-[11px] font-bold text-slate-300 uppercase tracking-wide">
<span className="w-2 h-2 rounded-full bg-slate-400 animate-pulse" /> Syncing live data
</span>
) : errored ? (
<span className="inline-flex items-center gap-1.5 text-[11px] font-bold text-rose-300 uppercase tracking-wide" title="Restart the dev server so the /hasura proxy is active.">
<span className="w-2 h-2 rounded-full bg-rose-400" /> Live data unavailable
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-[11px] font-bold text-emerald-300 uppercase tracking-wide">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" /> Live · {tenantName}
</span>
)}
</div>
</div>
{/* Reporting scope panel */}
<div className="flex flex-col items-start md:items-end gap-2 shrink-0">
<div className="inline-flex items-center gap-2 bg-white/10 backdrop-blur-md border border-white/15 rounded-xl px-3.5 py-2.5 shadow-sm">
<Clock size={14} className="text-purple-300" />
<span className="text-xs font-bold font-mono text-white tracking-tight">{fromdate} {todate}</span>
</div>
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Month-to-date reporting scope</span>
</div>
</div>
</div>
{/* Error hint */}
{errored && (
@@ -202,32 +149,36 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
)}
{/* KPI cards — all live from getordersummary / getinvoiceinsight */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-gutter">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
{kpis.map((kpi) => {
const Icon = kpi.icon;
return (
<div
key={kpi.title}
className="group relative flex flex-col overflow-hidden bg-white border border-slate-200/70 rounded-2xl p-5 shadow-[0_1px_2px_rgba(16,24,40,0.04)] transition-all duration-300 hover:-translate-y-1 hover:border-purple-200 hover:shadow-[0_16px_36px_rgba(16,24,40,0.10)]"
className="group relative flex items-center gap-3 overflow-hidden bg-white border border-slate-200/70 rounded-xl p-3 shadow-[0_1px_2px_rgba(16,24,40,0.04)] transition-all duration-300 hover:-translate-y-1 hover:border-purple-200 hover:shadow-[0_16px_36px_rgba(16,24,40,0.10)]"
>
{/* Gradient accent bar */}
<span className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${kpi.bar}`} />
<div className="flex items-start justify-between">
<div className={`h-11 w-11 rounded-xl flex items-center justify-center ring-1 group-hover:scale-110 transition-transform duration-300 ${kpi.chip}`}>
<Icon size={19} />
<span className={`absolute inset-y-0 left-0 w-1 bg-gradient-to-b ${kpi.bar}`} />
<div className={`h-10 w-10 shrink-0 rounded-lg flex items-center justify-center ring-1 group-hover:scale-110 transition-transform duration-300 ml-1 ${kpi.chip}`}>
<Icon size={18} />
</div>
<ArrowUpRight size={16} className="text-slate-300 group-hover:text-purple-400 transition-colors" />
</div>
<p className="text-[10px] font-bold text-slate-400 tracking-widest uppercase font-sans mt-4">
<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="flex items-center justify-between">
<p className="text-[9px] font-bold text-slate-400 tracking-widest uppercase font-sans truncate pr-2">
{kpi.title}
</p>
<p className="font-sans font-extrabold text-[28px] leading-tight text-slate-900 tracking-tight mt-1">
<ArrowUpRight size={12} className="text-slate-300 group-hover:text-purple-400 transition-colors shrink-0" />
</div>
<p className="font-sans font-extrabold text-lg leading-tight text-slate-900 tracking-tight mt-0.5 truncate">
{kpi.loading ? <span className="text-slate-300"></span> : kpi.display}
</p>
<p className="text-[11px] text-slate-400 font-medium mt-1.5 leading-snug">
<p className="text-[9px] text-slate-400 font-medium mt-0.5 leading-snug truncate">
{kpi.sub}
</p>
</div>
</div>
);
})}
</div>

View File

@@ -1037,6 +1037,8 @@ export interface CreateTenantLocationPayload {
city?: string;
state?: string;
postcode?: string;
latitude?: string;
longitude?: string;
applocationid?: number;
}
@@ -1050,6 +1052,8 @@ export interface CreateTenantInput {
city?: string;
state?: string;
postcode?: string;
latitude?: string;
longitude?: string;
approved?: number;
status?: string;
applocationid?: number;