mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-03 19:46:13 +01:00
Feat: Add export to CSV support (#292)
* install json2csv package * add necessary labels * add support convert the JSON to redable CSV format and export * add a popover to export btton and provide options for exporting to JSON and CSV * Use a DropdownMenu * Translations --------- Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Erstelle die Erste",
|
"createFirst": "Erstelle die Erste",
|
||||||
"noExpenses": "Deine Gruppe hat noch keine Ausgaben.",
|
"noExpenses": "Deine Gruppe hat noch keine Ausgaben.",
|
||||||
"exportJson": "Als JSON exportieren",
|
"exportJson": "Als JSON exportieren",
|
||||||
|
"exportCsv": "Als CSV exportieren",
|
||||||
"searchPlaceholder": "Suche nach einer Ausgabe…",
|
"searchPlaceholder": "Suche nach einer Ausgabe…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Wer bist du?",
|
"title": "Wer bist du?",
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
"create": "Create expense",
|
"create": "Create expense",
|
||||||
"createFirst": "Create the first one",
|
"createFirst": "Create the first one",
|
||||||
"noExpenses": "Your group doesn’t contain any expense yet.",
|
"noExpenses": "Your group doesn’t contain any expense yet.",
|
||||||
|
"export": "Export",
|
||||||
"exportJson": "Export to JSON",
|
"exportJson": "Export to JSON",
|
||||||
|
"exportCsv": "Export to CSV",
|
||||||
"searchPlaceholder": "Search for an expense…",
|
"searchPlaceholder": "Search for an expense…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Who are you?",
|
"title": "Who are you?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Crea el primero",
|
"createFirst": "Crea el primero",
|
||||||
"noExpenses": "Tu grupo aun no tiene gastos.",
|
"noExpenses": "Tu grupo aun no tiene gastos.",
|
||||||
"exportJson": "Exportar a JSON",
|
"exportJson": "Exportar a JSON",
|
||||||
|
"exportCsv": "Exportar a CSV",
|
||||||
"searchPlaceholder": "Busca un gasto…",
|
"searchPlaceholder": "Busca un gasto…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "¿Quién es usted?",
|
"title": "¿Quién es usted?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Lisää ensimmäinen kulu",
|
"createFirst": "Lisää ensimmäinen kulu",
|
||||||
"noExpenses": "Ryhmälläsi ei ole vielä yhtään kulua.",
|
"noExpenses": "Ryhmälläsi ei ole vielä yhtään kulua.",
|
||||||
"exportJson": "Vie JSON-tiedostoon",
|
"exportJson": "Vie JSON-tiedostoon",
|
||||||
|
"exportCsv": "Vie CSV-tiedostoon",
|
||||||
"searchPlaceholder": "Etsi kulua…",
|
"searchPlaceholder": "Etsi kulua…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Kuka olet?",
|
"title": "Kuka olet?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Créer la première :)",
|
"createFirst": "Créer la première :)",
|
||||||
"noExpenses": "Votre groupe n'a effectué aucune dépense pour le moment.",
|
"noExpenses": "Votre groupe n'a effectué aucune dépense pour le moment.",
|
||||||
"exportJson": "Exporter en JSON",
|
"exportJson": "Exporter en JSON",
|
||||||
|
"exportCsv": "Exporter en CSV",
|
||||||
"searchPlaceholder": "Rechercher une dépense…",
|
"searchPlaceholder": "Rechercher une dépense…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Qui êtes-vous ?",
|
"title": "Qui êtes-vous ?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Crea la prima",
|
"createFirst": "Crea la prima",
|
||||||
"noExpenses": "Il tuo gruppo non contiene ancora spese.",
|
"noExpenses": "Il tuo gruppo non contiene ancora spese.",
|
||||||
"exportJson": "Esporta file JSON",
|
"exportJson": "Esporta file JSON",
|
||||||
|
"exportCsv": "Esporta file CSV",
|
||||||
"searchPlaceholder": "Cerca una spesa…",
|
"searchPlaceholder": "Cerca una spesa…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Chi sei?",
|
"title": "Chi sei?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Stwórz swój pierwszy",
|
"createFirst": "Stwórz swój pierwszy",
|
||||||
"noExpenses": "Twoja grupa nie ma jeszcze żadnych wydatków.",
|
"noExpenses": "Twoja grupa nie ma jeszcze żadnych wydatków.",
|
||||||
"exportJson": "Eksportuj do JSONa",
|
"exportJson": "Eksportuj do JSONa",
|
||||||
|
"exportCsv": "Eksportuj do CSVa",
|
||||||
"searchPlaceholder": "Szukaj wydatku...",
|
"searchPlaceholder": "Szukaj wydatku...",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Kim jesteś?",
|
"title": "Kim jesteś?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Adaug-o pe prima",
|
"createFirst": "Adaug-o pe prima",
|
||||||
"noExpenses": "Grupul tău nu conține nicio cheltuială încă.",
|
"noExpenses": "Grupul tău nu conține nicio cheltuială încă.",
|
||||||
"exportJson": "Salvează în JSON",
|
"exportJson": "Salvează în JSON",
|
||||||
|
"exportCsv": "Salvează în CSV",
|
||||||
"searchPlaceholder": "Caută o cheltuială…",
|
"searchPlaceholder": "Caută o cheltuială…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Cum te numești?",
|
"title": "Cum te numești?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Создать первый расход",
|
"createFirst": "Создать первый расход",
|
||||||
"noExpenses": "У вашей группы пока что нет расходов.",
|
"noExpenses": "У вашей группы пока что нет расходов.",
|
||||||
"exportJson": "Экспортировать в JSON",
|
"exportJson": "Экспортировать в JSON",
|
||||||
|
"exportCsv": "Экспортировать в CSV",
|
||||||
"searchPlaceholder": "Поиск расходов…",
|
"searchPlaceholder": "Поиск расходов…",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Кто вы?",
|
"title": "Кто вы?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "Створіть першу витрату",
|
"createFirst": "Створіть першу витрату",
|
||||||
"noExpenses": "У вашій групі ще немає витрат",
|
"noExpenses": "У вашій групі ще немає витрат",
|
||||||
"exportJson": "Експортувати у JSON",
|
"exportJson": "Експортувати у JSON",
|
||||||
|
"exportCsv": "Експортувати у CSV",
|
||||||
"searchPlaceholder": "Пошук витрат...",
|
"searchPlaceholder": "Пошук витрат...",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "Хто ви?",
|
"title": "Хто ви?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "创建首个消费",
|
"createFirst": "创建首个消费",
|
||||||
"noExpenses": "你的群组内目前没有任何消费。",
|
"noExpenses": "你的群组内目前没有任何消费。",
|
||||||
"exportJson": "导出到JSON",
|
"exportJson": "导出到JSON",
|
||||||
|
"exportCsv": "导出到CSV",
|
||||||
"searchPlaceholder": "查找消费……",
|
"searchPlaceholder": "查找消费……",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "你是哪位?",
|
"title": "你是哪位?",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"createFirst": "新增第一筆消費紀錄",
|
"createFirst": "新增第一筆消費紀錄",
|
||||||
"noExpenses": "你的群組內目前沒有任何消費紀錄。",
|
"noExpenses": "你的群組內目前沒有任何消費紀錄。",
|
||||||
"exportJson": "匯出為 JSON",
|
"exportJson": "匯出為 JSON",
|
||||||
|
"exportCsv": "匯出為 CSV",
|
||||||
"searchPlaceholder": "搜尋消費紀錄……",
|
"searchPlaceholder": "搜尋消費紀錄……",
|
||||||
"ActiveUserModal": {
|
"ActiveUserModal": {
|
||||||
"title": "你是誰?",
|
"title": "你是誰?",
|
||||||
|
|||||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.5.4",
|
"@formatjs/intl-localematcher": "^0.5.4",
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@prisma/client": "^5.6.0",
|
"@prisma/client": "^5.6.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
@@ -5320,6 +5321,22 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@json2csv/formatters": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@json2csv/formatters/-/formatters-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-hjIk1H1TR4ydU5ntIENEPgoMGW+Q7mJ+537sDFDbsk+Y3EPl2i4NfFVjw0NJRgT+ihm8X30M67mA8AS6jPidSA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@json2csv/plainjs": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@json2csv/plainjs/-/plainjs-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-4Md7RPDCSYpmW1HWIpWBOqCd4vWfIqm53S3e/uzQ62iGi7L3r34fK/8nhOMEe+/eVfCx8+gdSCt1d74SlacQHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@json2csv/formatters": "^7.0.6",
|
||||||
|
"@streamparser/json": "^0.0.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.5",
|
"version": "14.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
|
||||||
@@ -8930,6 +8947,12 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@streamparser/json": {
|
||||||
|
"version": "0.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.20.tgz",
|
||||||
|
"integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/counter": {
|
"node_modules/@swc/counter": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.5.4",
|
"@formatjs/intl-localematcher": "^0.5.4",
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@prisma/client": "^5.6.0",
|
"@prisma/client": "^5.6.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
|
|||||||
142
src/app/groups/[groupId]/expenses/export/csv/route.ts
Normal file
142
src/app/groups/[groupId]/expenses/export/csv/route.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { Parser } from '@json2csv/plainjs'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import contentDisposition from 'content-disposition'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const splitModeLabel = {
|
||||||
|
EVENLY: 'Evenly',
|
||||||
|
BY_SHARES: 'Unevenly – By shares',
|
||||||
|
BY_PERCENTAGE: 'Unevenly – By percentage',
|
||||||
|
BY_AMOUNT: 'Unevenly – By amount',
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(isoDateString: Date): string {
|
||||||
|
const date = new Date(isoDateString)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0') // Months are zero-based
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}` // YYYY-MM-DD format
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request,
|
||||||
|
{ params: { groupId } }: { params: { groupId: string } },
|
||||||
|
) {
|
||||||
|
const group = await prisma.group.findUnique({
|
||||||
|
where: { id: groupId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
currency: true,
|
||||||
|
expenses: {
|
||||||
|
select: {
|
||||||
|
expenseDate: true,
|
||||||
|
title: true,
|
||||||
|
category: { select: { name: true } },
|
||||||
|
amount: true,
|
||||||
|
paidById: true,
|
||||||
|
paidFor: { select: { participantId: true, shares: true } },
|
||||||
|
isReimbursement: true,
|
||||||
|
splitMode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
participants: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return NextResponse.json({ error: 'Invalid group ID' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
CSV Structure:
|
||||||
|
|
||||||
|
--------------------------------------------------------------
|
||||||
|
| Date | Description | Category | Currency | Cost
|
||||||
|
--------------------------------------------------------------
|
||||||
|
| Is Reimbursement | Split mode | UserA | UserB
|
||||||
|
--------------------------------------------------------------
|
||||||
|
|
||||||
|
Columns:
|
||||||
|
- Date: The date of the expense.
|
||||||
|
- Description: A brief description of the expense.
|
||||||
|
- Category: The category of the expense (e.g., Food, Travel, etc.).
|
||||||
|
- Currency: The currency in which the expense is recorded.
|
||||||
|
- Cost: The amount spent.
|
||||||
|
- Is Reimbursement: Whether the expense is a reimbursement or not.
|
||||||
|
- Split mode: The method used to split the expense (e.g., Evenly, By shares, By percentage, By amount).
|
||||||
|
- UserA, UserB: User-specific data or balances (e.g., amount owed or contributed by each user).
|
||||||
|
|
||||||
|
Example Row:
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
| 2025-01-06 | Dinner with team | Food | ₹ | 5000 | No | Evenly | John | Jane
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ label: 'Date', value: 'date' },
|
||||||
|
{ label: 'Description', value: 'title' },
|
||||||
|
{ label: 'Category', value: 'categoryName' },
|
||||||
|
{ label: 'Currency', value: 'currency' },
|
||||||
|
{ label: 'Cost', value: 'amount' },
|
||||||
|
{ label: 'Is Reimbursement', value: 'isReimbursement' },
|
||||||
|
{ label: 'Split mode', value: 'splitMode' },
|
||||||
|
...group.participants.map((participant) => ({
|
||||||
|
label: participant.name,
|
||||||
|
value: participant.name,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
const expenses = group.expenses.map((expense) => ({
|
||||||
|
date: formatDate(expense.expenseDate),
|
||||||
|
title: expense.title,
|
||||||
|
categoryName: expense.category?.name || '',
|
||||||
|
currency: group.currency,
|
||||||
|
amount: (expense.amount / 100).toFixed(2),
|
||||||
|
isReimbursement: expense.isReimbursement ? 'Yes' : 'No',
|
||||||
|
splitMode: splitModeLabel[expense.splitMode],
|
||||||
|
...Object.fromEntries(
|
||||||
|
group.participants.map((participant) => {
|
||||||
|
const { totalShares, participantShare } = expense.paidFor.reduce(
|
||||||
|
(acc, { participantId, shares }) => {
|
||||||
|
acc.totalShares += shares
|
||||||
|
if (participantId === participant.id) {
|
||||||
|
acc.participantShare = shares
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{ totalShares: 0, participantShare: 0 },
|
||||||
|
)
|
||||||
|
|
||||||
|
const isPaidByParticipant = expense.paidById === participant.id
|
||||||
|
const participantAmountShare = +(
|
||||||
|
((expense.amount / totalShares) * participantShare) /
|
||||||
|
100
|
||||||
|
).toFixed(2)
|
||||||
|
|
||||||
|
return [
|
||||||
|
participant.name,
|
||||||
|
participantAmountShare * (isPaidByParticipant ? 1 : -1),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const json2csvParser = new Parser({ fields })
|
||||||
|
const csv = json2csvParser.parse(expenses)
|
||||||
|
|
||||||
|
const date = new Date().toISOString().split('T')[0]
|
||||||
|
const filename = `Spliit Export - ${group.name} - ${date}.csv`
|
||||||
|
|
||||||
|
// \uFEFF character is added at the beginning of the CSV content to ensure that it is interpreted as UTF-8 with BOM (Byte Order Mark), which helps some applications correctly interpret the encoding.
|
||||||
|
return new NextResponse(`\uFEFF${csv}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/csv; charset=utf-8',
|
||||||
|
'Content-Disposition': contentDisposition(filename),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ActiveUserModal } from '@/app/groups/[groupId]/expenses/active-user-modal'
|
import { ActiveUserModal } from '@/app/groups/[groupId]/expenses/active-user-modal'
|
||||||
import { CreateFromReceiptButton } from '@/app/groups/[groupId]/expenses/create-from-receipt-button'
|
import { CreateFromReceiptButton } from '@/app/groups/[groupId]/expenses/create-from-receipt-button'
|
||||||
import { ExpenseList } from '@/app/groups/[groupId]/expenses/expense-list'
|
import { ExpenseList } from '@/app/groups/[groupId]/expenses/expense-list'
|
||||||
|
import ExportButton from '@/app/groups/[groupId]/export-button'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { Download, Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -40,16 +41,7 @@ export default function GroupExpensesPageClient({
|
|||||||
<CardDescription>{t('description')}</CardDescription>
|
<CardDescription>{t('description')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardHeader className="p-4 sm:p-6 flex flex-row space-y-0 gap-2">
|
<CardHeader className="p-4 sm:p-6 flex flex-row space-y-0 gap-2">
|
||||||
<Button variant="secondary" size="icon" asChild>
|
<ExportButton groupId={groupId} />
|
||||||
<Link
|
|
||||||
prefetch={false}
|
|
||||||
href={`/groups/${groupId}/expenses/export/json`}
|
|
||||||
target="_blank"
|
|
||||||
title={t('exportJson')}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
{enableReceiptExtract && <CreateFromReceiptButton />}
|
{enableReceiptExtract && <CreateFromReceiptButton />}
|
||||||
<Button asChild size="icon">
|
<Button asChild size="icon">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
53
src/app/groups/[groupId]/export-button.tsx
Normal file
53
src/app/groups/[groupId]/export-button.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Download, FileDown, FileJson } from 'lucide-react'
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function ExportButton({ groupId }: { groupId: string }) {
|
||||||
|
const t = useTranslations('Expenses')
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button title={t('export')} variant="secondary" size="icon">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
prefetch={false}
|
||||||
|
href={`/groups/${groupId}/expenses/export/json`}
|
||||||
|
target="_blank"
|
||||||
|
title={t('exportJson')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileJson className="w-4 h-4" />
|
||||||
|
<p>{t('exportJson')}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
prefetch={false}
|
||||||
|
href={`/groups/${groupId}/expenses/export/csv`}
|
||||||
|
target="_blank"
|
||||||
|
title={t('exportCsv')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileDown className="w-4 h-4" />
|
||||||
|
<p>{t('exportCsv')}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user