ShadCN Vaults

Back to Blocks

Feature-Rich Billing Dashboard

Dashboard Bills Block

Dashboard BillsComponent

Feature-Rich Billing Dashboard

Feature-rich dashboard for plan selection, add-on toggling, and invoice management.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

For questions about licensing, please contact the project maintainers.