refactor: overhaul project structure, update configuration, and improve consistency across admin and investor dashboard components.

This commit is contained in:
sazzadulalambd
2026-05-21 11:40:03 +06:00
parent 7332f85512
commit b83325b8e3
3 changed files with 189 additions and 21 deletions

1
JML Submodule

Submodule JML added at 7332f85512

View File

@@ -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">&times;</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>

View File

@@ -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') {