refactor: overhaul project structure, update configuration, and improve consistency across admin and investor dashboard components.
This commit is contained in:
1
JML
Submodule
1
JML
Submodule
Submodule JML added at 7332f85512
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, Save, Trash2, X } from 'lucide-react';
|
||||
import { Plus, Save, Trash2, X, Gift } from 'lucide-react';
|
||||
import { CompanySettings } from '../page';
|
||||
|
||||
interface PlanSelectionProps {
|
||||
@@ -14,6 +14,108 @@ interface PlanSelectionProps {
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
// Reusable Free Service Conditions editor
|
||||
function FreeServiceConditions({
|
||||
conditions,
|
||||
accentColor,
|
||||
onChange,
|
||||
}: {
|
||||
conditions: { months: number; freeServices: number }[];
|
||||
accentColor: string;
|
||||
onChange: (updated: { months: number; freeServices: number }[]) => void;
|
||||
}) {
|
||||
const addCondition = () => {
|
||||
onChange([...conditions, { months: 3, freeServices: 1 }]);
|
||||
};
|
||||
|
||||
const removeCondition = (i: number) => {
|
||||
onChange(conditions.filter((_, idx) => idx !== i));
|
||||
};
|
||||
|
||||
const updateCondition = (i: number, field: 'months' | 'freeServices', value: number) => {
|
||||
const updated = conditions.map((c, idx) => idx === i ? { ...c, [field]: value } : c);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-amber-50 border border-amber-100 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-amber-600" />
|
||||
<label className="text-xs font-semibold text-amber-700 uppercase tracking-wide">
|
||||
Free Service Conditions
|
||||
</label>
|
||||
<span className="text-[10px] text-amber-500 font-medium bg-amber-100 px-2 py-0.5 rounded-full">
|
||||
e.g. "3 months → 2 free services"
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCondition}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-semibold transition-all ${accentColor} text-white hover:opacity-90`}
|
||||
>
|
||||
<Plus className="w-3 h-3" /> Add Condition
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{conditions.length === 0 && (
|
||||
<p className="text-xs text-amber-400 italic text-center py-2">
|
||||
No free service conditions set. Click "Add Condition" to add one.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{conditions.map((cond, i) => (
|
||||
<div key={i} className="flex items-center gap-3 bg-white border border-amber-100 rounded-lg px-3 py-2 group">
|
||||
{/* Month input */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs text-slate-500 font-medium shrink-0">Month:</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={cond.months}
|
||||
onChange={(e) => updateCondition(i, 'months', parseInt(e.target.value) || 1)}
|
||||
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-slate-300 text-sm">→</span>
|
||||
|
||||
{/* Free services input */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs text-slate-500 font-medium shrink-0">Free Services:</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={cond.freeServices}
|
||||
onChange={(e) => updateCondition(i, 'freeServices', parseInt(e.target.value) || 1)}
|
||||
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Preview badge */}
|
||||
<span className="flex-1 text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-100 rounded-full px-2.5 py-1 text-center truncate">
|
||||
{cond.months} {cond.months === 1 ? 'month' : 'months'} → {cond.freeServices} free service{cond.freeServices !== 1 ? 's' : ''} free
|
||||
</span>
|
||||
|
||||
{/* Remove */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCondition(i)}
|
||||
className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded-md transition-all opacity-0 group-hover:opacity-100"
|
||||
title="Remove condition"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlanSelection({
|
||||
settings,
|
||||
setSettings,
|
||||
@@ -172,6 +274,18 @@ export default function PlanSelection({
|
||||
</div>
|
||||
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
|
||||
</div>
|
||||
|
||||
{/* Free Service Conditions */}
|
||||
<FreeServiceConditions
|
||||
conditions={plan.freeServiceConditions ?? []}
|
||||
accentColor="bg-blue-600"
|
||||
onChange={(updated) => {
|
||||
const plans = [...settings.plans.singleRent];
|
||||
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
|
||||
setSettings({ ...settings, plans: { ...settings.plans, singleRent: plans } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
|
||||
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
|
||||
@@ -312,6 +426,18 @@ export default function PlanSelection({
|
||||
</div>
|
||||
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
|
||||
</div>
|
||||
|
||||
{/* Free Service Conditions */}
|
||||
<FreeServiceConditions
|
||||
conditions={plan.freeServiceConditions ?? []}
|
||||
accentColor="bg-purple-600"
|
||||
onChange={(updated) => {
|
||||
const plans = [...settings.plans.rentToOwn];
|
||||
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
|
||||
setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: plans } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
|
||||
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
|
||||
@@ -452,6 +578,18 @@ export default function PlanSelection({
|
||||
</div>
|
||||
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
|
||||
</div>
|
||||
|
||||
{/* Free Service Conditions */}
|
||||
<FreeServiceConditions
|
||||
conditions={plan.freeServiceConditions ?? []}
|
||||
accentColor="bg-green-600"
|
||||
onChange={(updated) => {
|
||||
const plans = [...settings.plans.shareEv];
|
||||
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
|
||||
setSettings({ ...settings, plans: { ...settings.plans, shareEv: plans } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
|
||||
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
|
||||
@@ -465,26 +603,16 @@ export default function PlanSelection({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{deleteModal.type !== null && deleteModal.idx !== null && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm">
|
||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<Trash2 className="w-5 h-5 text-red-500" /> Delete Plan
|
||||
</h3>
|
||||
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="text-slate-400 hover:text-slate-600 text-2xl">×</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-slate-600">Are you sure you want to delete this plan? This action cannot be undone.</p>
|
||||
{deleteModal.type && deleteModal.idx !== null && settings.plans[deleteModal.type][deleteModal.idx] && (
|
||||
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-sm font-medium text-slate-700">{settings.plans[deleteModal.type][deleteModal.idx].name}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm">Cancel</button>
|
||||
<button onClick={handleDeletePlan} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700">Delete Plan</button>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{deleteModal.type !== null && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-800">Delete Plan?</h3>
|
||||
<p className="text-sm text-slate-500">This will permanently remove the plan. This action cannot be undone.</p>
|
||||
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
|
||||
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="px-4 py-2 border border-slate-200 text-slate-500 rounded-lg text-sm font-medium hover:bg-slate-50">Cancel</button>
|
||||
<button onClick={handleDeletePlan} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700">Delete Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface CompanySettings {
|
||||
monthlyPenalty2: number;
|
||||
monthlyPenalty3: number;
|
||||
ficoSharePercent: number;
|
||||
freeServiceConditions: { months: number; freeServices: number }[];
|
||||
description: string;
|
||||
}[];
|
||||
rentToOwn: {
|
||||
@@ -151,6 +152,7 @@ export interface CompanySettings {
|
||||
profit: number;
|
||||
ficoRentSharePercent: number;
|
||||
ficoProfitSharePercent: number;
|
||||
freeServiceConditions: { months: number; freeServices: number }[];
|
||||
description: string;
|
||||
}[];
|
||||
shareEv: {
|
||||
@@ -179,6 +181,7 @@ export interface CompanySettings {
|
||||
monthlyPenalty3: number;
|
||||
totalMonthlySubscription: number;
|
||||
ficoSharePercent: number;
|
||||
freeServiceConditions: { months: number; freeServices: number }[];
|
||||
description: string;
|
||||
}[];
|
||||
investment: {
|
||||
@@ -498,6 +501,10 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty2: 30000,
|
||||
monthlyPenalty3: 50000,
|
||||
ficoSharePercent: 50,
|
||||
freeServiceConditions: [
|
||||
{ months: 3, freeServices: 2 },
|
||||
{ months: 6, freeServices: 4 },
|
||||
],
|
||||
description: 'Premium single person rental plan with extra benefits',
|
||||
},
|
||||
{
|
||||
@@ -519,6 +526,10 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty2: 22000,
|
||||
monthlyPenalty3: 40000,
|
||||
ficoSharePercent: 45,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
{ months: 3, freeServices: 2 },
|
||||
],
|
||||
description: 'Standard single person rental plan',
|
||||
},
|
||||
{
|
||||
@@ -540,6 +551,9 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty2: 18000,
|
||||
monthlyPenalty3: 30000,
|
||||
ficoSharePercent: 40,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
],
|
||||
description: 'Economy single person rental plan',
|
||||
}
|
||||
],
|
||||
@@ -568,6 +582,10 @@ const initialSettings: CompanySettings = {
|
||||
profit: 20000,
|
||||
ficoRentSharePercent: 50,
|
||||
ficoProfitSharePercent: 45,
|
||||
freeServiceConditions: [
|
||||
{ months: 3, freeServices: 2 },
|
||||
{ months: 6, freeServices: 4 },
|
||||
],
|
||||
description: 'Premium rent to own plan with high-end EV',
|
||||
},
|
||||
{
|
||||
@@ -594,6 +612,10 @@ const initialSettings: CompanySettings = {
|
||||
profit: 15000,
|
||||
ficoRentSharePercent: 45,
|
||||
ficoProfitSharePercent: 45,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
{ months: 3, freeServices: 2 },
|
||||
],
|
||||
description: 'Standard rent to own plan',
|
||||
},
|
||||
{
|
||||
@@ -620,6 +642,9 @@ const initialSettings: CompanySettings = {
|
||||
profit: 15000,
|
||||
ficoRentSharePercent: 40,
|
||||
ficoProfitSharePercent: 40,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
],
|
||||
description: 'Economy rent to own plan',
|
||||
}
|
||||
],
|
||||
@@ -650,6 +675,10 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty3: 35000,
|
||||
totalMonthlySubscription: 16800,
|
||||
ficoSharePercent: 50,
|
||||
freeServiceConditions: [
|
||||
{ months: 3, freeServices: 2 },
|
||||
{ months: 6, freeServices: 4 },
|
||||
],
|
||||
description: 'Premium shared EV with premium bikes',
|
||||
},
|
||||
{
|
||||
@@ -678,6 +707,10 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty3: 25000,
|
||||
totalMonthlySubscription: 11200,
|
||||
ficoSharePercent: 45,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
{ months: 3, freeServices: 2 },
|
||||
],
|
||||
description: 'Standard shared EV plan',
|
||||
},
|
||||
{
|
||||
@@ -706,6 +739,9 @@ const initialSettings: CompanySettings = {
|
||||
monthlyPenalty3: 20000,
|
||||
totalMonthlySubscription: 8400,
|
||||
ficoSharePercent: 40,
|
||||
freeServiceConditions: [
|
||||
{ months: 2, freeServices: 1 },
|
||||
],
|
||||
description: 'Economy shared EV plan',
|
||||
}
|
||||
],
|
||||
@@ -1127,6 +1163,7 @@ export default function CompanySettingsPage() {
|
||||
monthlyPenalty2: 22000,
|
||||
monthlyPenalty3: 40000,
|
||||
ficoSharePercent: 45,
|
||||
freeServiceConditions: [],
|
||||
description: '',
|
||||
} : type === 'rentToOwn' ? {
|
||||
id: newId,
|
||||
@@ -1152,6 +1189,7 @@ export default function CompanySettingsPage() {
|
||||
profit: 15000,
|
||||
ficoRentSharePercent: 45,
|
||||
ficoProfitSharePercent: 45,
|
||||
freeServiceConditions: [],
|
||||
description: '',
|
||||
} : {
|
||||
id: newId,
|
||||
@@ -1179,6 +1217,7 @@ export default function CompanySettingsPage() {
|
||||
monthlyPenalty3: 25000,
|
||||
totalMonthlySubscription: 11200,
|
||||
ficoSharePercent: 45,
|
||||
freeServiceConditions: [],
|
||||
description: '',
|
||||
};
|
||||
if (type === 'singleRent') {
|
||||
|
||||
Reference in New Issue
Block a user