Preview
Full width desktop view
Code
bills-11.tsx
1"use client";
2
3import * as React from "react";
4import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5import { Button } from "@/components/ui/button";
6import { Badge } from "@/components/ui/badge";
7import { Separator } from "@/components/ui/separator";
8import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9import { Checkbox } from "@/components/ui/checkbox";
10import { cn } from "@/lib/utils";
11import {
12 Check,
13 CreditCard,
14 Calendar,
15 DollarSign,
16 Users,
17 Zap,
18 Shield,
19 Star,
20 AlertCircle,
21 Clock,
22 Building,
23} from "lucide-react";
24
25interface Plan {
26 id: string;
27 name: string;
28 description: string;
29 price: {
30 monthly: number;
31 yearly: number;
32 };
33 features: string[];
34 popular?: boolean;
35 current?: boolean;
36}
37
38interface AddOn {
39 id: string;
40 name: string;
41 description: string;
42 price: {
43 monthly: number;
44 yearly: number;
45 };
46 category: string;
47 enabled?: boolean;
48}
49
50interface Bill {
51 id: string;
52 description: string;
53 amount: number;
54 dueDate: string;
55 status: "paid" | "pending" | "overdue";
56 planId?: string;
57 addOnIds?: string[];
58}
59
60interface CustomerAdminDashboardProps {
61 plans?: Plan[];
62 addOns?: AddOn[];
63 bills?: Bill[];
64 currentPlan?: string;
65 onPlanSelect?: (planId: string) => void;
66 onAddOnToggle?: (addOnId: string, enabled: boolean) => void;
67}
68
69const defaultPlans: Plan[] = [
70 {
71 id: "starter",
72 name: "Starter",
73 description: "Perfect for small teams getting started",
74 price: { monthly: 29, yearly: 290 },
75 features: [
76 "Up to 5 team members",
77 "10GB storage",
78 "Basic analytics",
79 "Email support",
80 "Standard integrations",
81 ],
82 },
83 {
84 id: "professional",
85 name: "Professional",
86 description: "Advanced features for growing businesses",
87 price: { monthly: 79, yearly: 790 },
88 features: [
89 "Up to 25 team members",
90 "100GB storage",
91 "Advanced analytics",
92 "Priority support",
93 "All integrations",
94 "Custom workflows",
95 "API access",
96 ],
97 popular: true,
98 current: true,
99 },
100 {
101 id: "enterprise",
102 name: "Enterprise",
103 description: "Complete solution for large organizations",
104 price: { monthly: 199, yearly: 1990 },
105 features: [
106 "Unlimited team members",
107 "1TB storage",
108 "Enterprise analytics",
109 "24/7 phone support",
110 "Custom integrations",
111 "Advanced security",
112 "Dedicated account manager",
113 "SLA guarantee",
114 ],
115 },
116];
117
118const defaultAddOns: AddOn[] = [
119 {
120 id: "extra-storage",
121 name: "Extra Storage",
122 description: "Additional 500GB storage space",
123 price: { monthly: 15, yearly: 150 },
124 category: "Storage",
125 enabled: true,
126 },
127 {
128 id: "advanced-security",
129 name: "Advanced Security",
130 description: "Enhanced security features and compliance",
131 price: { monthly: 25, yearly: 250 },
132 category: "Security",
133 enabled: false,
134 },
135 {
136 id: "premium-support",
137 name: "Premium Support",
138 description: "24/7 priority support with dedicated agent",
139 price: { monthly: 35, yearly: 350 },
140 category: "Support",
141 enabled: true,
142 },
143 {
144 id: "analytics-pro",
145 name: "Analytics Pro",
146 description: "Advanced reporting and business intelligence",
147 price: { monthly: 20, yearly: 200 },
148 category: "Analytics",
149 enabled: false,
150 },
151];
152
153const defaultBills: Bill[] = [
154 {
155 id: "bill-1",
156 description: "Professional Plan - Monthly",
157 amount: 79,
158 dueDate: "2024-02-15",
159 status: "pending",
160 planId: "professional",
161 },
162 {
163 id: "bill-2",
164 description: "Extra Storage Add-on",
165 amount: 15,
166 dueDate: "2024-02-15",
167 status: "pending",
168 addOnIds: ["extra-storage"],
169 },
170 {
171 id: "bill-3",
172 description: "Premium Support Add-on",
173 amount: 35,
174 dueDate: "2024-02-15",
175 status: "pending",
176 addOnIds: ["premium-support"],
177 },
178 {
179 id: "bill-4",
180 description: "Professional Plan - January",
181 amount: 79,
182 dueDate: "2024-01-15",
183 status: "paid",
184 planId: "professional",
185 },
186];
187
188export function CustomerAdminDashboard({
189 plans = defaultPlans,
190 addOns = defaultAddOns,
191 bills = defaultBills,
192 currentPlan = "professional",
193 onPlanSelect,
194 onAddOnToggle,
195}: CustomerAdminDashboardProps) {
196 const [selectedPlan, setSelectedPlan] = React.useState(currentPlan);
197 const [billingInterval, setBillingInterval] = React.useState<
198 "monthly" | "yearly"
199 >("monthly");
200 const [enabledAddOns, setEnabledAddOns] = React.useState<Set<string>>(
201 new Set(addOns.filter((addon) => addon.enabled).map((addon) => addon.id))
202 );
203
204 const handlePlanSelect = (planId: string) => {
205 setSelectedPlan(planId);
206 onPlanSelect?.(planId);
207 };
208
209 const handleAddOnToggle = (addOnId: string, enabled: boolean) => {
210 const newEnabledAddOns = new Set(enabledAddOns);
211 if (enabled) {
212 newEnabledAddOns.add(addOnId);
213 } else {
214 newEnabledAddOns.delete(addOnId);
215 }
216 setEnabledAddOns(newEnabledAddOns);
217 onAddOnToggle?.(addOnId, enabled);
218 };
219
220 const currentPlanData = plans.find((plan) => plan.id === selectedPlan);
221 const pendingBills = bills.filter((bill) => bill.status === "pending");
222 const overdueBills = bills.filter((bill) => bill.status === "overdue");
223
224 const calculateTotal = () => {
225 const planPrice = currentPlanData?.price[billingInterval] || 0;
226 const addOnPrice = Array.from(enabledAddOns).reduce((total, addOnId) => {
227 const addOn = addOns.find((a) => a.id === addOnId);
228 return total + (addOn?.price[billingInterval] || 0);
229 }, 0);
230 return planPrice + addOnPrice;
231 };
232
233 const getBillStatusColor = (status: string) => {
234 switch (status) {
235 case "paid":
236 return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
237 case "pending":
238 return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
239 case "overdue":
240 return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
241 default:
242 return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
243 }
244 };
245
246 const getBillStatusIcon = (status: string) => {
247 switch (status) {
248 case "paid":
249 return <Check className="h-4 w-4" />;
250 case "pending":
251 return <Clock className="h-4 w-4" />;
252 case "overdue":
253 return <AlertCircle className="h-4 w-4" />;
254 default:
255 return null;
256 }
257 };
258
259 return (
260 <div className="min-h-screen bg-background p-6">
261 <div className="max-w-7xl mx-auto space-y-8">
262 {/* Header */}
263 <div className="flex items-center justify-between">
264 <div>
265 <h1 className="text-3xl font-bold text-foreground">
266 Billing Dashboard
267 </h1>
268 <p className="text-muted-foreground mt-2">
269 Manage your subscription plans and billing
270 </p>
271 </div>
272 <div className="flex items-center gap-4">
273 <Badge variant="outline" className="flex items-center gap-2">
274 <Building className="h-4 w-4" />
275 Enterprise Account
276 </Badge>
277 </div>
278 </div>
279
280 {/* Quick Stats */}
281 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
282 <Card>
283 <CardContent className="p-6">
284 <div className="flex items-center gap-4">
285 <div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
286 <DollarSign className="h-6 w-6 text-blue-600 dark:text-blue-400" />
287 </div>
288 <div>
289 <p className="text-sm text-muted-foreground">Monthly Cost</p>
290 <p className="text-2xl font-bold">${calculateTotal()}</p>
291 </div>
292 </div>
293 </CardContent>
294 </Card>
295
296 <Card>
297 <CardContent className="p-6">
298 <div className="flex items-center gap-4">
299 <div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
300 <Users className="h-6 w-6 text-green-600 dark:text-green-400" />
301 </div>
302 <div>
303 <p className="text-sm text-muted-foreground">Active Users</p>
304 <p className="text-2xl font-bold">18/25</p>
305 </div>
306 </div>
307 </CardContent>
308 </Card>
309
310 <Card>
311 <CardContent className="p-6">
312 <div className="flex items-center gap-4">
313 <div className="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
314 <Clock className="h-6 w-6 text-yellow-600 dark:text-yellow-400" />
315 </div>
316 <div>
317 <p className="text-sm text-muted-foreground">Pending Bills</p>
318 <p className="text-2xl font-bold">{pendingBills.length}</p>
319 </div>
320 </div>
321 </CardContent>
322 </Card>
323
324 <Card>
325 <CardContent className="p-6">
326 <div className="flex items-center gap-4">
327 <div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
328 <Zap className="h-6 w-6 text-purple-600 dark:text-purple-400" />
329 </div>
330 <div>
331 <p className="text-sm text-muted-foreground">Add-ons</p>
332 <p className="text-2xl font-bold">{enabledAddOns.size}</p>
333 </div>
334 </div>
335 </CardContent>
336 </Card>
337 </div>
338
339 <Tabs defaultValue="plans" className="space-y-6">
340 <TabsList className="grid w-full grid-cols-3">
341 <TabsTrigger value="plans">Subscription Plans</TabsTrigger>
342 <TabsTrigger value="addons">Add-ons</TabsTrigger>
343 <TabsTrigger value="billing">Current Bills</TabsTrigger>
344 </TabsList>
345
346 {/* Plans Tab */}
347 <TabsContent value="plans" className="space-y-6">
348 <div className="flex items-center justify-between">
349 <h2 className="text-2xl font-semibold">Choose Your Plan</h2>
350 <div className="flex items-center gap-2 bg-muted p-1 rounded-lg">
351 <button
352 onClick={() => setBillingInterval("monthly")}
353 className={cn(
354 "px-3 py-1 rounded-md text-sm transition-colors",
355 billingInterval === "monthly"
356 ? "bg-background text-foreground shadow-sm"
357 : "text-muted-foreground hover:text-foreground"
358 )}
359 >
360 Monthly
361 </button>
362 <button
363 onClick={() => setBillingInterval("yearly")}
364 className={cn(
365 "px-3 py-1 rounded-md text-sm transition-colors",
366 billingInterval === "yearly"
367 ? "bg-background text-foreground shadow-sm"
368 : "text-muted-foreground hover:text-foreground"
369 )}
370 >
371 Yearly
372 </button>
373 </div>
374 </div>
375
376 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
377 {plans.map((plan) => (
378 <Card
379 key={plan.id}
380 className={cn(
381 "relative transition-all duration-200",
382 selectedPlan === plan.id && "ring-2 ring-primary",
383 plan.popular && "border-primary"
384 )}
385 >
386 {plan.popular && (
387 <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
388 <Badge className="bg-primary text-primary-foreground">
389 <Star className="h-3 w-3 mr-1" />
390 Most Popular
391 </Badge>
392 </div>
393 )}
394
395 <CardHeader className="text-center pb-4">
396 <CardTitle className="text-xl">{plan.name}</CardTitle>
397 <p className="text-muted-foreground text-sm">
398 {plan.description}
399 </p>
400 <div className="mt-4">
401 <span className="text-3xl font-bold">
402 ${plan.price[billingInterval]}
403 </span>
404 <span className="text-muted-foreground">
405 /{billingInterval === "monthly" ? "month" : "year"}
406 </span>
407 </div>
408 </CardHeader>
409
410 <CardContent className="space-y-4">
411 <ul className="space-y-2">
412 {plan.features.map((feature, index) => (
413 <li
414 key={index}
415 className="flex items-center gap-2 text-sm"
416 >
417 <Check className="h-4 w-4 text-green-500 flex-shrink-0" />
418 {feature}
419 </li>
420 ))}
421 </ul>
422
423 <Button
424 onClick={() => handlePlanSelect(plan.id)}
425 variant={selectedPlan === plan.id ? "default" : "outline"}
426 className="w-full"
427 >
428 {selectedPlan === plan.id
429 ? "Current Plan"
430 : "Select Plan"}
431 </Button>
432 </CardContent>
433 </Card>
434 ))}
435 </div>
436 </TabsContent>
437
438 {/* Add-ons Tab */}
439 <TabsContent value="addons" className="space-y-6">
440 <h2 className="text-2xl font-semibold">Available Add-ons</h2>
441
442 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
443 {addOns.map((addOn) => (
444 <Card key={addOn.id} className="transition-all duration-200">
445 <CardContent className="p-6">
446 <div className="flex items-start justify-between">
447 <div className="flex-1">
448 <div className="flex items-center gap-3 mb-2">
449 <Checkbox
450 checked={enabledAddOns.has(addOn.id)}
451 onCheckedChange={(checked) =>
452 handleAddOnToggle(addOn.id, !!checked)
453 }
454 />
455 <div>
456 <h3 className="font-semibold">{addOn.name}</h3>
457 <Badge variant="secondary" className="text-xs">
458 {addOn.category}
459 </Badge>
460 </div>
461 </div>
462 <p className="text-sm text-muted-foreground mb-3">
463 {addOn.description}
464 </p>
465 <div className="flex items-center gap-2">
466 <span className="font-semibold">
467 ${addOn.price[billingInterval]}
468 </span>
469 <span className="text-muted-foreground text-sm">
470 /{billingInterval === "monthly" ? "month" : "year"}
471 </span>
472 </div>
473 </div>
474 </div>
475 </CardContent>
476 </Card>
477 ))}
478 </div>
479
480 {/* Add-ons Summary */}
481 <Card>
482 <CardHeader>
483 <CardTitle className="flex items-center gap-2">
484 <CreditCard className="h-5 w-5" />
485 Add-ons Summary
486 </CardTitle>
487 </CardHeader>
488 <CardContent>
489 <div className="space-y-3">
490 {Array.from(enabledAddOns).map((addOnId) => {
491 const addOn = addOns.find((a) => a.id === addOnId);
492 if (!addOn) return null;
493
494 return (
495 <div
496 key={addOnId}
497 className="flex justify-between items-center"
498 >
499 <span className="text-sm">{addOn.name}</span>
500 <span className="font-medium">
501 ${addOn.price[billingInterval]}
502 </span>
503 </div>
504 );
505 })}
506
507 {enabledAddOns.size === 0 && (
508 <p className="text-muted-foreground text-sm">
509 No add-ons selected
510 </p>
511 )}
512
513 {enabledAddOns.size > 0 && (
514 <>
515 <Separator />
516 <div className="flex justify-between items-center font-semibold">
517 <span>Total Add-ons</span>
518 <span>
519 $
520 {Array.from(enabledAddOns).reduce(
521 (total, addOnId) => {
522 const addOn = addOns.find(
523 (a) => a.id === addOnId
524 );
525 return (
526 total + (addOn?.price[billingInterval] || 0)
527 );
528 },
529 0
530 )}
531 </span>
532 </div>
533 </>
534 )}
535 </div>
536 </CardContent>
537 </Card>
538 </TabsContent>
539
540 {/* Billing Tab */}
541 <TabsContent value="billing" className="space-y-6">
542 <div className="flex items-center justify-between">
543 <h2 className="text-2xl font-semibold">Current Bills</h2>
544 <Button variant="outline" className="flex items-center gap-2">
545 <Calendar className="h-4 w-4" />
546 View History
547 </Button>
548 </div>
549
550 {/* Alerts for overdue bills */}
551 {overdueBills.length > 0 && (
552 <Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
553 <CardContent className="p-4">
554 <div className="flex items-center gap-2 text-red-800 dark:text-red-200">
555 <AlertCircle className="h-5 w-5" />
556 <span className="font-medium">
557 You have {overdueBills.length} overdue bill(s)
558 </span>
559 </div>
560 </CardContent>
561 </Card>
562 )}
563
564 {/* Bills List */}
565 <div className="space-y-4">
566 {bills.map((bill) => (
567 <Card key={bill.id}>
568 <CardContent className="p-6">
569 <div className="flex items-center justify-between">
570 <div className="flex items-center gap-4">
571 <div className="p-2 bg-muted rounded-lg">
572 {getBillStatusIcon(bill.status)}
573 </div>
574 <div>
575 <h3 className="font-semibold">{bill.description}</h3>
576 <p className="text-sm text-muted-foreground">
577 Due: {new Date(bill.dueDate).toLocaleDateString()}
578 </p>
579 </div>
580 </div>
581
582 <div className="flex items-center gap-4">
583 <Badge className={getBillStatusColor(bill.status)}>
584 {bill.status.charAt(0).toUpperCase() +
585 bill.status.slice(1)}
586 </Badge>
587 <span className="font-semibold text-lg">
588 ${bill.amount}
589 </span>
590 {bill.status === "pending" && (
591 <Button size="sm">Pay Now</Button>
592 )}
593 </div>
594 </div>
595 </CardContent>
596 </Card>
597 ))}
598 </div>
599
600 {/* Total Summary */}
601 <Card>
602 <CardHeader>
603 <CardTitle className="flex items-center gap-2">
604 <DollarSign className="h-5 w-5" />
605 Billing Summary
606 </CardTitle>
607 </CardHeader>
608 <CardContent>
609 <div className="space-y-3">
610 <div className="flex justify-between items-center">
611 <span>Current Plan ({currentPlanData?.name})</span>
612 <span>${currentPlanData?.price[billingInterval] || 0}</span>
613 </div>
614
615 {Array.from(enabledAddOns).map((addOnId) => {
616 const addOn = addOns.find((a) => a.id === addOnId);
617 if (!addOn) return null;
618
619 return (
620 <div
621 key={addOnId}
622 className="flex justify-between items-center"
623 >
624 <span>{addOn.name}</span>
625 <span>${addOn.price[billingInterval]}</span>
626 </div>
627 );
628 })}
629
630 <Separator />
631
632 <div className="flex justify-between items-center font-semibold text-lg">
633 <span>
634 Total{" "}
635 {billingInterval === "monthly" ? "Monthly" : "Yearly"}
636 </span>
637 <span>${calculateTotal()}</span>
638 </div>
639
640 <div className="flex justify-between items-center text-sm text-muted-foreground">
641 <span>Next billing date</span>
642 <span>February 15, 2024</span>
643 </div>
644 </div>
645 </CardContent>
646 </Card>
647 </TabsContent>
648 </Tabs>
649 </div>
650 </div>
651 );
652}
653
654export default function CustomerAdminDashboardDemo() {
655 return <CustomerAdminDashboard />;
656}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardcheckboxseparatortabs
Local Components
/lib/utils