mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Added financial report
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
ApiProduces,
|
ApiProduces,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { ReportsService } from "./reports.service";
|
import { ReportsService } from "./reports.service";
|
||||||
import { BlockedSeatsRevenueLossQueryDto, GenerateReportDto } from "./reports.dto";
|
import { BlockedSeatsRevenueLossQueryDto, FinanceSummaryQueryDto, GenerateReportDto } from "./reports.dto";
|
||||||
import { PassengerStaff } from "../../common/passenger-guards";
|
import { PassengerStaff } from "../../common/passenger-guards";
|
||||||
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
||||||
|
|
||||||
@@ -83,6 +83,35 @@ export class ReportsController {
|
|||||||
return this.service.getPaymentDiscrepancyBySchedule(scheduleId, { search, seatClass, sort });
|
return this.service.getPaymentDiscrepancyBySchedule(scheduleId, { search, seatClass, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Finance Summary ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Get("finance")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Finance summary — revenue by period, origin/destination segment, and payment method",
|
||||||
|
description:
|
||||||
|
"Revenue collected in the window (PaymentIntent.paidAt), grouped by day/week/month, origin → " +
|
||||||
|
"destination station pair, and payment method. Filter by originStationId and/or destinationStationId " +
|
||||||
|
"independently to query any station-pair segment (A→B, A→D, B→C), not just a whole predefined route. " +
|
||||||
|
"Returns per-bucket rows plus roll-ups by period, segment, and method for charting.",
|
||||||
|
})
|
||||||
|
getFinanceSummary(@Query() query: FinanceSummaryQueryDto) {
|
||||||
|
return this.service.getFinanceSummary(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("finance/export")
|
||||||
|
@ApiOperation({ summary: "Finance summary as CSV — one row per period + route + payment method" })
|
||||||
|
@ApiProduces("text/csv")
|
||||||
|
@ApiOkResponse({ description: "CSV export", schema: { type: "string" } })
|
||||||
|
async exportFinanceSummary(@Query() query: FinanceSummaryQueryDto, @Res() res: Response): Promise<void> {
|
||||||
|
const csv = await this.service.exportFinanceSummaryCsv(query);
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="finance-summary-${new Date().toISOString().split("T")[0]}.csv"`,
|
||||||
|
);
|
||||||
|
res.send(csv);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Blocked Seat Revenue Loss ──────────────────────────────────────────────
|
// ── Blocked Seat Revenue Loss ──────────────────────────────────────────────
|
||||||
|
|
||||||
@Get("blocked-seats-revenue-loss")
|
@Get("blocked-seats-revenue-loss")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IsString, IsDateString, IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator';
|
import { IsString, IsDateString, IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { PaymentMethodType } from '@prisma/client';
|
||||||
import { SeatBlockReasonCategory } from '../seats/seats.dto';
|
import { SeatBlockReasonCategory } from '../seats/seats.dto';
|
||||||
|
|
||||||
export enum ReportType {
|
export enum ReportType {
|
||||||
@@ -103,3 +104,31 @@ export class BlockedSeatsRevenueLossQueryDto {
|
|||||||
})
|
})
|
||||||
@IsOptional() @IsEnum(BlockedSeatsLossSortBy) sortBy?: BlockedSeatsLossSortBy;
|
@IsOptional() @IsEnum(BlockedSeatsLossSortBy) sortBy?: BlockedSeatsLossSortBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Finance Summary ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export enum FinanceGranularity {
|
||||||
|
DAILY = 'daily',
|
||||||
|
WEEKLY = 'weekly',
|
||||||
|
MONTHLY = 'monthly',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FinanceSummaryQueryDto {
|
||||||
|
@ApiProperty({ example: '2026-07-01', description: 'Start of the window, inclusive, matched on PaymentIntent.paidAt.' })
|
||||||
|
@IsDateString() dateFrom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2026-07-31', description: 'End of the window, inclusive, matched on PaymentIntent.paidAt.' })
|
||||||
|
@IsDateString() dateTo: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: FinanceGranularity, default: FinanceGranularity.DAILY })
|
||||||
|
@IsOptional() @IsEnum(FinanceGranularity) granularity?: FinanceGranularity;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Restrict to bookings departing from this station.' })
|
||||||
|
@IsOptional() @IsString() originStationId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Restrict to bookings arriving at this station.' })
|
||||||
|
@IsOptional() @IsString() destinationStationId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: PaymentMethodType, description: 'Restrict to payments made with this method.' })
|
||||||
|
@IsOptional() @IsEnum(PaymentMethodType) method?: PaymentMethodType;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { FareEngineService } from "../fare-engine/fare-engine.service";
|
|||||||
import {
|
import {
|
||||||
BlockedSeatsLossSortBy,
|
BlockedSeatsLossSortBy,
|
||||||
BlockedSeatsRevenueLossQueryDto,
|
BlockedSeatsRevenueLossQueryDto,
|
||||||
|
FinanceGranularity,
|
||||||
|
FinanceSummaryQueryDto,
|
||||||
GenerateReportDto,
|
GenerateReportDto,
|
||||||
ReportType,
|
ReportType,
|
||||||
} from "./reports.dto";
|
} from "./reports.dto";
|
||||||
@@ -84,6 +86,33 @@ function toCsvCell(value: string | number): string {
|
|||||||
return `"${String(value).replace(/"/g, '""')}"`;
|
return `"${String(value).replace(/"/g, '""')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buckets a paid-at timestamp into the requested reporting period, keyed so buckets sort
|
||||||
|
* chronologically as plain strings. Weekly buckets are labelled by their Monday (UTC).
|
||||||
|
*/
|
||||||
|
function periodKeyFor(date: Date, granularity: FinanceGranularity): string {
|
||||||
|
if (granularity === FinanceGranularity.MONTHLY) {
|
||||||
|
return date.toISOString().slice(0, 7);
|
||||||
|
}
|
||||||
|
if (granularity === FinanceGranularity.WEEKLY) {
|
||||||
|
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||||
|
const isoDay = d.getUTCDay() || 7; // Monday=1 .. Sunday=7
|
||||||
|
d.setUTCDate(d.getUTCDate() - (isoDay - 1));
|
||||||
|
return d.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
return date.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceBucket {
|
||||||
|
period: string;
|
||||||
|
originStationId: string;
|
||||||
|
destinationStationId: string;
|
||||||
|
segmentLabel: string;
|
||||||
|
method: string;
|
||||||
|
bookingCount: number;
|
||||||
|
revenueEtbMinor: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReportsService {
|
export class ReportsService {
|
||||||
private readonly logger = new Logger(ReportsService.name);
|
private readonly logger = new Logger(ReportsService.name);
|
||||||
@@ -1051,6 +1080,161 @@ export class ReportsService {
|
|||||||
return { totalActualEtbMinor, totalPaidEtbMinor, byMethod, rows };
|
return { totalActualEtbMinor, totalPaidEtbMinor, byMethod, rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Finance Summary ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revenue collected in the window, grouped by reporting period (day/week/month), origin →
|
||||||
|
* destination station pair, and payment method — the shape finance reconciles against
|
||||||
|
* provider settlement statements.
|
||||||
|
*
|
||||||
|
* Grouped by the booking's own origin/destination, not the parent Route — a route like
|
||||||
|
* "Sebeta - Dire Dawa" has intermediate stops, and a passenger may have booked any
|
||||||
|
* sub-segment of it (e.g. Lebu → Adama). Filtering by station lets finance ask about any
|
||||||
|
* A→B pair, not just whole routes.
|
||||||
|
*
|
||||||
|
* Bucketed on `PaymentIntent.paidAt` (cash actually received), not `Booking.createdAt`,
|
||||||
|
* so a booking made in one period but paid in another lands in the period it was paid.
|
||||||
|
*/
|
||||||
|
async getFinanceSummary(query: FinanceSummaryQueryDto) {
|
||||||
|
const dateFrom = new Date(query.dateFrom + "T00:00:00.000Z");
|
||||||
|
const dateTo = new Date(query.dateTo + "T23:59:59.999Z");
|
||||||
|
const granularity = query.granularity ?? FinanceGranularity.DAILY;
|
||||||
|
|
||||||
|
const rateRows = await this.prisma.currencyExchangeRate.findMany({
|
||||||
|
where: { toCurrency: "ETB" as any },
|
||||||
|
orderBy: { effectiveDate: "desc" },
|
||||||
|
});
|
||||||
|
const rateToEtb = new Map<string, number>();
|
||||||
|
for (const r of rateRows) {
|
||||||
|
if (!rateToEtb.has(r.fromCurrency)) rateToEtb.set(r.fromCurrency, Number(r.rate));
|
||||||
|
}
|
||||||
|
const toEtbMinor = (minor: number, currency: string): number => {
|
||||||
|
if (currency === "ETB") return minor;
|
||||||
|
const rate = rateToEtb.get(currency);
|
||||||
|
return rate ? Math.round(minor * rate) : minor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bookings = await this.prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
paymentIntent: {
|
||||||
|
paidAt: { gte: dateFrom, lte: dateTo },
|
||||||
|
...(query.method ? { method: query.method } : {}),
|
||||||
|
},
|
||||||
|
...(query.originStationId ? { originStationId: query.originStationId } : {}),
|
||||||
|
...(query.destinationStationId ? { destinationStationId: query.destinationStationId } : {}),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
totalMinor: true,
|
||||||
|
currency: true,
|
||||||
|
originStationId: true,
|
||||||
|
destinationStationId: true,
|
||||||
|
schedule: { select: { originStationId: true, destinationStationId: true } },
|
||||||
|
paymentIntent: { select: { paidAt: true, method: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Booking.originStationId/destinationStationId are set on every create path (guest and
|
||||||
|
// authenticated booking both pass them from the DTO); the schedule's own endpoints are
|
||||||
|
// only a fallback for the rare legacy row that predates those columns.
|
||||||
|
const stationIds = new Set<string>();
|
||||||
|
for (const b of bookings) {
|
||||||
|
const origin = b.originStationId ?? b.schedule.originStationId;
|
||||||
|
const destination = b.destinationStationId ?? b.schedule.destinationStationId;
|
||||||
|
if (origin) stationIds.add(origin);
|
||||||
|
if (destination) stationIds.add(destination);
|
||||||
|
}
|
||||||
|
const stations = stationIds.size > 0
|
||||||
|
? await this.prisma.station.findMany({ where: { id: { in: [...stationIds] } }, select: { id: true, name: true } })
|
||||||
|
: [];
|
||||||
|
const stationName = new Map(stations.map((s) => [s.id, s.name]));
|
||||||
|
|
||||||
|
const buckets = new Map<string, FinanceBucket>();
|
||||||
|
const bucketFor = (
|
||||||
|
period: string,
|
||||||
|
originStationId: string,
|
||||||
|
destinationStationId: string,
|
||||||
|
segmentLabel: string,
|
||||||
|
method: string,
|
||||||
|
): FinanceBucket => {
|
||||||
|
const key = `${period}|${originStationId}|${destinationStationId}|${method}`;
|
||||||
|
let bucket = buckets.get(key);
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = { period, originStationId, destinationStationId, segmentLabel, method, bookingCount: 0, revenueEtbMinor: 0 };
|
||||||
|
buckets.set(key, bucket);
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const b of bookings) {
|
||||||
|
const pi = b.paymentIntent!;
|
||||||
|
const period = periodKeyFor(pi.paidAt!, granularity);
|
||||||
|
const originStationId = b.originStationId ?? b.schedule.originStationId ?? "UNKNOWN";
|
||||||
|
const destinationStationId = b.destinationStationId ?? b.schedule.destinationStationId ?? "UNKNOWN";
|
||||||
|
const segmentLabel = `${stationName.get(originStationId) ?? "Unknown"} → ${stationName.get(destinationStationId) ?? "Unknown"}`;
|
||||||
|
const bucket = bucketFor(period, originStationId, destinationStationId, segmentLabel, pi.method);
|
||||||
|
bucket.bookingCount += 1;
|
||||||
|
bucket.revenueEtbMinor += toEtbMinor(b.totalMinor, b.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [...buckets.values()].sort((a, b) =>
|
||||||
|
a.period === b.period
|
||||||
|
? a.segmentLabel.localeCompare(b.segmentLabel) || a.method.localeCompare(b.method)
|
||||||
|
: a.period.localeCompare(b.period),
|
||||||
|
);
|
||||||
|
|
||||||
|
const totals = rows.reduce(
|
||||||
|
(acc, r) => {
|
||||||
|
acc.bookingCount += r.bookingCount;
|
||||||
|
acc.revenueEtbMinor += r.revenueEtbMinor;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ bookingCount: 0, revenueEtbMinor: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const rollUp = (keyOf: (r: FinanceBucket) => string, labelOf: (r: FinanceBucket) => string) => {
|
||||||
|
const map = new Map<string, { key: string; label: string; revenueEtbMinor: number; bookingCount: number }>();
|
||||||
|
for (const r of rows) {
|
||||||
|
const key = keyOf(r);
|
||||||
|
let entry = map.get(key);
|
||||||
|
if (!entry) {
|
||||||
|
entry = { key, label: labelOf(r), revenueEtbMinor: 0, bookingCount: 0 };
|
||||||
|
map.set(key, entry);
|
||||||
|
}
|
||||||
|
entry.revenueEtbMinor += r.revenueEtbMinor;
|
||||||
|
entry.bookingCount += r.bookingCount;
|
||||||
|
}
|
||||||
|
return [...map.values()].sort((a, b) => b.revenueEtbMinor - a.revenueEtbMinor);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
granularity,
|
||||||
|
dateFrom: query.dateFrom,
|
||||||
|
dateTo: query.dateTo,
|
||||||
|
currency: "ETB",
|
||||||
|
totals,
|
||||||
|
byPeriod: rollUp((r) => r.period, (r) => r.period),
|
||||||
|
bySegment: rollUp((r) => `${r.originStationId}|${r.destinationStationId}`, (r) => r.segmentLabel),
|
||||||
|
byMethod: rollUp((r) => r.method, (r) => r.method),
|
||||||
|
rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSV of the finance summary, one row per period + origin/destination segment + payment method. */
|
||||||
|
async exportFinanceSummaryCsv(query: FinanceSummaryQueryDto): Promise<string> {
|
||||||
|
const report = await this.getFinanceSummary(query);
|
||||||
|
|
||||||
|
const headers = ["Period", "Origin → Destination", "Payment Method", "Bookings", "Revenue (ETB)"];
|
||||||
|
const rows = report.rows.map((r) => [
|
||||||
|
r.period,
|
||||||
|
r.segmentLabel,
|
||||||
|
r.method,
|
||||||
|
r.bookingCount,
|
||||||
|
(r.revenueEtbMinor / 100).toFixed(2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [headers, ...rows].map((row) => row.map(toCsvCell).join(",")).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async getPaymentDiscrepancyBySchedule(scheduleId: string, params: {
|
async getPaymentDiscrepancyBySchedule(scheduleId: string, params: {
|
||||||
search?: string;
|
search?: string;
|
||||||
seatClass?: string;
|
seatClass?: string;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"html-to-image": "^1.11.11",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"next": "^14.2.0",
|
"next": "^14.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Banknote, BookOpen, FileSpreadsheet, Receipt } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Bar, BarChart, CartesianGrid, Line, LineChart,
|
||||||
|
ResponsiveContainer, Tooltip as RechartsTooltip, XAxis, YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { financeApi, type FinanceGranularity, type FinanceSummaryFilters } from '@/lib/api/finance';
|
||||||
|
import { buildFinanceWorkbook, type ChartImage } from '@/lib/export/finance-workbook';
|
||||||
|
import ActionButton from '@/components/ui/ActionButton';
|
||||||
|
import Pagination from '@/components/ui/Pagination';
|
||||||
|
import Skeleton from '@/components/ui/Skeleton';
|
||||||
|
import { usePagination } from '@/lib/use-pagination';
|
||||||
|
import { formatCurrency } from '@/lib/utils';
|
||||||
|
import { categoricalColor, getChartPalette } from '@/lib/chart-palette';
|
||||||
|
import { useTheme } from '@/lib/theme-store';
|
||||||
|
|
||||||
|
interface StationOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed order so a method keeps its colour/slot when the method filter narrows the set.
|
||||||
|
const PAYMENT_METHOD_ORDER = ['TELEBIRR', 'CBE_BIRR', 'EBIRR', 'WAAFI', 'CARD', 'WALLET', 'DMONEY', 'CAC_BANK', 'CBE_BILL'] as const;
|
||||||
|
const PAYMENT_METHOD_LABELS: Record<string, string> = {
|
||||||
|
TELEBIRR: 'Telebirr',
|
||||||
|
CBE_BIRR: 'CBE Birr',
|
||||||
|
EBIRR: 'eBirr',
|
||||||
|
WAAFI: 'Waafi',
|
||||||
|
CARD: 'Card',
|
||||||
|
WALLET: 'Wallet',
|
||||||
|
DMONEY: 'DMoney',
|
||||||
|
CAC_BANK: 'CAC Bank',
|
||||||
|
CBE_BILL: 'CBE Bill',
|
||||||
|
};
|
||||||
|
function methodLabel(method: string): string {
|
||||||
|
return PAYMENT_METHOD_LABELS[method] ?? method;
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodLabel(period: string, granularity: FinanceGranularity): string {
|
||||||
|
if (granularity === 'monthly') {
|
||||||
|
return new Date(`${period}-01T00:00:00`).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||||
|
}
|
||||||
|
return new Date(`${period}T00:00:00`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABLE_PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
/** Mirrors the loaded layout's shape (KPI tiles, two charts, method breakdown, detail table) so nothing jumps when data arrives. */
|
||||||
|
function FinanceReportSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={i} className="card flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
<Skeleton className="h-7 w-7 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-7 w-32" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{Array.from({ length: 2 }).map((_, i) => (
|
||||||
|
<div key={i} className="card">
|
||||||
|
<Skeleton className="h-4 w-40 mb-4" />
|
||||||
|
<Skeleton className="h-[280px] w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<Skeleton className="h-4 w-56 mb-2" />
|
||||||
|
<Skeleton className="h-3 w-32 mb-4" />
|
||||||
|
<Skeleton className="h-7 w-full mb-4" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-5 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-0">
|
||||||
|
<div className="px-4 pt-4 pb-3">
|
||||||
|
<Skeleton className="h-4 w-52" />
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pb-4 space-y-2">
|
||||||
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-8 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FinanceReportPage() {
|
||||||
|
const isDark = useTheme((s) => s.isDark);
|
||||||
|
const palette = getChartPalette(isDark);
|
||||||
|
|
||||||
|
const [dateRangePreset, setDateRangePreset] = useState('90');
|
||||||
|
const [customFrom, setCustomFrom] = useState('');
|
||||||
|
const [customTo, setCustomTo] = useState('');
|
||||||
|
const [granularity, setGranularity] = useState<FinanceGranularity>('daily');
|
||||||
|
const [originStationId, setOriginStationId] = useState('');
|
||||||
|
const [destinationStationId, setDestinationStationId] = useState('');
|
||||||
|
const [method, setMethod] = useState('');
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
|
const trendCardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const segmentCardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const methodCardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { dateFrom, dateTo } = useMemo(() => {
|
||||||
|
const end = new Date();
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
if (dateRangePreset === 'custom') {
|
||||||
|
if (customFrom && customTo) {
|
||||||
|
return customFrom <= customTo
|
||||||
|
? { dateFrom: customFrom, dateTo: customTo }
|
||||||
|
: { dateFrom: customTo, dateTo: customFrom };
|
||||||
|
}
|
||||||
|
const fallbackStart = new Date(end);
|
||||||
|
fallbackStart.setDate(end.getDate() - 90);
|
||||||
|
return {
|
||||||
|
dateFrom: fallbackStart.toISOString().split('T')[0],
|
||||||
|
dateTo: end.toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = new Date(end);
|
||||||
|
start.setDate(end.getDate() - Number(dateRangePreset));
|
||||||
|
return {
|
||||||
|
dateFrom: start.toISOString().split('T')[0],
|
||||||
|
dateTo: end.toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
}, [dateRangePreset, customFrom, customTo]);
|
||||||
|
|
||||||
|
const filters: FinanceSummaryFilters = useMemo(
|
||||||
|
() => ({
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
granularity,
|
||||||
|
originStationId: originStationId || undefined,
|
||||||
|
destinationStationId: destinationStationId || undefined,
|
||||||
|
method: method || undefined,
|
||||||
|
}),
|
||||||
|
[dateFrom, dateTo, granularity, originStationId, destinationStationId, method],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: stations = [] } = useQuery<StationOption[]>({
|
||||||
|
queryKey: ['stations'],
|
||||||
|
queryFn: () => apiClient.get<StationOption[]>('/stations'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching, isError } = useQuery({
|
||||||
|
queryKey: ['reports-finance', filters],
|
||||||
|
placeholderData: (previous) => previous,
|
||||||
|
queryFn: () => financeApi.getSummary(filters),
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.rows ?? [];
|
||||||
|
const pg = usePagination(rows, TABLE_PAGE_SIZE);
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setDateRangePreset('90');
|
||||||
|
setCustomFrom('');
|
||||||
|
setCustomTo('');
|
||||||
|
setGranularity('daily');
|
||||||
|
setOriginStationId('');
|
||||||
|
setDestinationStationId('');
|
||||||
|
setMethod('');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Captures a chart card as a PNG data URL, sized to the card's actual on-screen pixels. */
|
||||||
|
const captureCard = async (node: HTMLDivElement | null): Promise<ChartImage | undefined> => {
|
||||||
|
if (!node) return undefined;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
const dataUrl = await toPng(node, { pixelRatio: 2, cacheBust: true, backgroundColor: palette.surface });
|
||||||
|
return { dataUrl, width: Math.round(rect.width), height: Math.round(rect.height) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const doExport = async () => {
|
||||||
|
if (!data) return;
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const [trend, segment, method_] = await Promise.all([
|
||||||
|
captureCard(trendCardRef.current),
|
||||||
|
captureCard(segmentCardRef.current),
|
||||||
|
captureCard(methodCardRef.current),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stationLabel = (id: string) => stations.find((s) => s.id === id)?.name ?? 'Any';
|
||||||
|
|
||||||
|
const blob = await buildFinanceWorkbook({
|
||||||
|
report: data,
|
||||||
|
filters: {
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
granularity,
|
||||||
|
originLabel: originStationId ? stationLabel(originStationId) : 'Any',
|
||||||
|
destinationLabel: destinationStationId ? stationLabel(destinationStationId) : 'Any',
|
||||||
|
methodLabel: method ? methodLabel(method) : 'All',
|
||||||
|
},
|
||||||
|
methodLabel,
|
||||||
|
periodLabel,
|
||||||
|
images: { trend, segment, method: method_ },
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `finance-summary-${dateFrom}-to-${dateTo}.xlsx`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const trendData = useMemo(
|
||||||
|
() =>
|
||||||
|
(data?.byPeriod ?? [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.key.localeCompare(b.key))
|
||||||
|
.map((p) => ({
|
||||||
|
label: periodLabel(p.key, data!.granularity),
|
||||||
|
revenue: p.revenueEtbMinor / 100,
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const segmentData = useMemo(
|
||||||
|
() =>
|
||||||
|
(data?.bySegment ?? [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.revenueEtbMinor - a.revenueEtbMinor)
|
||||||
|
.map((r) => ({ label: r.label, revenue: r.revenueEtbMinor })),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const methodBreakdown = useMemo(() => {
|
||||||
|
const rowsByMethod = data?.byMethod ?? [];
|
||||||
|
const total = rowsByMethod.reduce((sum, r) => sum + r.revenueEtbMinor, 0);
|
||||||
|
return rowsByMethod
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => PAYMENT_METHOD_ORDER.indexOf(a.key as any) - PAYMENT_METHOD_ORDER.indexOf(b.key as any))
|
||||||
|
.map((r) => ({
|
||||||
|
...r,
|
||||||
|
color: categoricalColor(palette, PAYMENT_METHOD_ORDER.indexOf(r.key as any)),
|
||||||
|
sharePercent: total > 0 ? (r.revenueEtbMinor / total) * 100 : 0,
|
||||||
|
}));
|
||||||
|
}, [data, palette]);
|
||||||
|
|
||||||
|
const totals = data?.totals;
|
||||||
|
const hasData = (totals?.bookingCount ?? 0) > 0;
|
||||||
|
const avgPerBookingMinor = hasData ? Math.round(totals!.revenueEtbMinor / totals!.bookingCount) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Finance Summary</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Revenue collected by period, origin/destination, and payment method — for daily, weekly, or monthly finance reporting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ActionButton icon={FileSpreadsheet} variant="secondary" onClick={doExport} loading={exporting} disabled={!hasData}>
|
||||||
|
Export
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6">
|
||||||
|
<div>
|
||||||
|
<label className="label">Date Range</label>
|
||||||
|
<select className="input" value={dateRangePreset} onChange={(e) => setDateRangePreset(e.target.value)}>
|
||||||
|
<option value="7">Last 7 Days</option>
|
||||||
|
<option value="30">Last 30 Days</option>
|
||||||
|
<option value="90">Last 90 Days</option>
|
||||||
|
<option value="custom">Custom Range</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{dateRangePreset === 'custom' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="label">Start Date</label>
|
||||||
|
<input type="date" className="input" value={customFrom} onChange={(e) => setCustomFrom(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">End Date</label>
|
||||||
|
<input type="date" className="input" value={customTo} onChange={(e) => setCustomTo(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="label">Granularity</label>
|
||||||
|
<select className="input" value={granularity} onChange={(e) => setGranularity(e.target.value as FinanceGranularity)}>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Origin</label>
|
||||||
|
<select className="input" value={originStationId} onChange={(e) => setOriginStationId(e.target.value)}>
|
||||||
|
<option value="">Any origin</option>
|
||||||
|
{stations.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Destination</label>
|
||||||
|
<select className="input" value={destinationStationId} onChange={(e) => setDestinationStationId(e.target.value)}>
|
||||||
|
<option value="">Any destination</option>
|
||||||
|
{stations.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Payment Method</label>
|
||||||
|
<select className="input" value={method} onChange={(e) => setMethod(e.target.value)}>
|
||||||
|
<option value="">All methods</option>
|
||||||
|
{PAYMENT_METHOD_ORDER.map((m) => (
|
||||||
|
<option key={m} value={m}>{methodLabel(m)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center justify-between">
|
||||||
|
<button type="button" onClick={resetFilters} className="text-xs text-primary hover:underline">
|
||||||
|
Reset filters
|
||||||
|
</button>
|
||||||
|
{isFetching && <span className="text-xs text-muted-foreground">Refreshing…</span>}
|
||||||
|
</div>
|
||||||
|
{isError && <p className="text-xs text-red-500 mt-3">Failed to load the finance summary. Check the filters and try again.</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && !data ? (
|
||||||
|
<FinanceReportSkeleton />
|
||||||
|
) : !hasData ? (
|
||||||
|
<div className="card py-16 text-center text-muted-foreground">
|
||||||
|
<Banknote className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||||
|
<p>No paid bookings in this window.</p>
|
||||||
|
<p className="text-xs mt-1">Widen the date range, or clear the origin/destination/method filters.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={isFetching ? 'opacity-60 transition-opacity space-y-6' : 'space-y-6'}>
|
||||||
|
{/* KPI tiles */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="card flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Revenue</p>
|
||||||
|
<div className="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 p-1.5">
|
||||||
|
<Banknote className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400 tabular-nums mt-1">
|
||||||
|
{formatCurrency(totals!.revenueEtbMinor, 'ETB')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="card flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Bookings</p>
|
||||||
|
<div className="rounded-lg bg-blue-100 dark:bg-blue-900/30 p-1.5">
|
||||||
|
<BookOpen className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold tabular-nums mt-1">{totals!.bookingCount.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="card flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Avg. per Booking</p>
|
||||||
|
<div className="rounded-lg bg-amber-100 dark:bg-amber-900/30 p-1.5">
|
||||||
|
<Receipt className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold tabular-nums mt-1">{formatCurrency(avgPerBookingMinor, 'ETB')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trend + route charts */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="card" ref={trendCardRef}>
|
||||||
|
<h3 className="text-base font-semibold mb-4">
|
||||||
|
Revenue Trend <span className="text-xs font-normal text-muted-foreground">(ETB, {granularity})</span>
|
||||||
|
</h3>
|
||||||
|
{trendData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={280}>
|
||||||
|
<LineChart data={trendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke={palette.grid} />
|
||||||
|
<XAxis dataKey="label" tick={{ fontSize: 11, fill: palette.textMuted }} />
|
||||||
|
<YAxis tick={{ fontSize: 11, fill: palette.textMuted }} />
|
||||||
|
<RechartsTooltip formatter={(value: number) => `ETB ${Math.round(value).toLocaleString()}`} />
|
||||||
|
<Line type="monotone" dataKey="revenue" name="Revenue" stroke={palette.sequential} dot={{ r: 3 }} strokeWidth={2} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="h-[280px] flex items-center justify-center text-muted-foreground text-sm">No data for selected range</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card" ref={segmentCardRef}>
|
||||||
|
<h3 className="text-base font-semibold mb-4">Revenue by Segment</h3>
|
||||||
|
{segmentData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={280}>
|
||||||
|
<BarChart data={segmentData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke={palette.grid} />
|
||||||
|
<XAxis dataKey="label" tick={{ fontSize: 11, fill: palette.textMuted }} />
|
||||||
|
<YAxis tick={{ fontSize: 11, fill: palette.textMuted }} />
|
||||||
|
<RechartsTooltip formatter={(value: number) => `ETB ${Math.round(value).toLocaleString()}`} />
|
||||||
|
<Bar dataKey="revenue" fill={palette.sequential} radius={[3, 3, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="h-[280px] flex items-center justify-center text-muted-foreground text-sm">No data for selected range</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment method breakdown — part-to-whole stacked bar + legend table */}
|
||||||
|
<div className="card" ref={methodCardRef}>
|
||||||
|
<h3 className="text-base font-semibold text-foreground">Revenue by Payment Method</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 mb-4">Share of revenue, in ETB</p>
|
||||||
|
<div
|
||||||
|
className="flex w-full h-7 rounded-md overflow-hidden"
|
||||||
|
role="img"
|
||||||
|
aria-label={`Revenue by payment method: ${methodBreakdown
|
||||||
|
.map((m) => `${methodLabel(m.key)} ${m.sharePercent.toFixed(0)}%`)
|
||||||
|
.join(', ')}`}
|
||||||
|
>
|
||||||
|
{methodBreakdown.map((m, i) => (
|
||||||
|
<div
|
||||||
|
key={m.key}
|
||||||
|
className="h-full"
|
||||||
|
style={{ width: `${m.sharePercent}%`, background: m.color, marginRight: i < methodBreakdown.length - 1 ? 2 : 0 }}
|
||||||
|
title={`${methodLabel(m.key)} — ${formatCurrency(m.revenueEtbMinor, 'ETB')}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-sm mt-4">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
<th className="text-left font-medium py-2">Method</th>
|
||||||
|
<th className="text-right font-medium py-2">Bookings</th>
|
||||||
|
<th className="text-right font-medium py-2">Share</th>
|
||||||
|
<th className="text-right font-medium py-2">Revenue</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
{methodBreakdown.map((m) => (
|
||||||
|
<tr key={m.key}>
|
||||||
|
<td className="py-2">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="h-2.5 w-2.5 rounded-sm shrink-0" style={{ background: m.color }} aria-hidden="true" />
|
||||||
|
<span className="text-foreground">{methodLabel(m.key)}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 text-right tabular-nums text-muted-foreground">{m.bookingCount.toLocaleString()}</td>
|
||||||
|
<td className="py-2 text-right tabular-nums text-muted-foreground">{m.sharePercent.toFixed(1)}%</td>
|
||||||
|
<td className="py-2 text-right tabular-nums text-foreground font-medium">{formatCurrency(m.revenueEtbMinor, 'ETB')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detail table */}
|
||||||
|
<div className="card p-0">
|
||||||
|
<div className="px-4 pt-4 pb-3">
|
||||||
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
Period × Segment × Method detail
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
|
<tr>
|
||||||
|
{['Period', 'Segment', 'Method', 'Bookings', 'Revenue'].map((h) => (
|
||||||
|
<th key={h} className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{pg.paged.map((r, i) => (
|
||||||
|
<tr key={`${r.period}-${r.originStationId}-${r.destinationStationId}-${r.method}-${i}`} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-foreground">{periodLabel(r.period, granularity)}</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-muted-foreground">{r.segmentLabel}</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-muted-foreground">{methodLabel(r.method)}</td>
|
||||||
|
<td className="px-4 py-3 tabular-nums whitespace-nowrap">{r.bookingCount.toLocaleString()}</td>
|
||||||
|
<td className="px-4 py-3 tabular-nums whitespace-nowrap font-medium">{formatCurrency(r.revenueEtbMinor, 'ETB')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{pg.paged.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="py-8 text-center text-sm text-muted-foreground">No rows on this page</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<Pagination currentPage={pg.page} totalPages={pg.totalPages} onPageChange={pg.setPage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -123,6 +123,7 @@ const navigationSections: { title: string; items: NavItem[] }[] = [
|
|||||||
title: 'Analytics & Reports',
|
title: 'Analytics & Reports',
|
||||||
items: [
|
items: [
|
||||||
{ name: 'Overall', href: '/reports/overall', icon: BarChart3, permission: PERMS.reports.view },
|
{ name: 'Overall', href: '/reports/overall', icon: BarChart3, permission: PERMS.reports.view },
|
||||||
|
{ name: 'Finance', href: '/reports/finance', icon: DollarSign, permission: PERMS.reports.view },
|
||||||
{ name: 'Seats', href: '/reports/seats', icon: Armchair, permission: PERMS.reports.view },
|
{ name: 'Seats', href: '/reports/seats', icon: Armchair, permission: PERMS.reports.view },
|
||||||
{ name: 'Blocked Seats', href: '/reports/blocked-seats', icon: Ban, permission: PERMS.reports.view },
|
{ name: 'Blocked Seats', href: '/reports/blocked-seats', icon: Ban, permission: PERMS.reports.view },
|
||||||
{ name: 'Passengers', href: '/reports/passengers', icon: Users, permission: PERMS.reports.view },
|
{ name: 'Passengers', href: '/reports/passengers', icon: Users, permission: PERMS.reports.view },
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A shimmering placeholder block. Give it the size/shape of the content it stands in for. */
|
||||||
|
export default function Skeleton({ className }: SkeletonProps) {
|
||||||
|
return <div className={cn('skeleton', className)} aria-hidden="true" />;
|
||||||
|
}
|
||||||
65
apps/edr-passenger-web/backoffice/src/lib/api/finance.ts
Normal file
65
apps/edr-passenger-web/backoffice/src/lib/api/finance.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
|
||||||
|
export type FinanceGranularity = 'daily' | 'weekly' | 'monthly';
|
||||||
|
|
||||||
|
export interface FinanceSummaryFilters {
|
||||||
|
dateFrom: string;
|
||||||
|
dateTo: string;
|
||||||
|
granularity?: FinanceGranularity;
|
||||||
|
originStationId?: string;
|
||||||
|
destinationStationId?: string;
|
||||||
|
method?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceBucketRow {
|
||||||
|
period: string;
|
||||||
|
originStationId: string;
|
||||||
|
destinationStationId: string;
|
||||||
|
segmentLabel: string;
|
||||||
|
method: string;
|
||||||
|
bookingCount: number;
|
||||||
|
revenueEtbMinor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceRollupRow {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
revenueEtbMinor: number;
|
||||||
|
bookingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceTotals {
|
||||||
|
bookingCount: number;
|
||||||
|
revenueEtbMinor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceSummaryReport {
|
||||||
|
granularity: FinanceGranularity;
|
||||||
|
dateFrom: string;
|
||||||
|
dateTo: string;
|
||||||
|
currency: string;
|
||||||
|
totals: FinanceTotals;
|
||||||
|
byPeriod: FinanceRollupRow[];
|
||||||
|
bySegment: FinanceRollupRow[];
|
||||||
|
byMethod: FinanceRollupRow[];
|
||||||
|
rows: FinanceBucketRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops blanks so the API applies its own defaults (granularity=daily, no station/method filter). */
|
||||||
|
function toParams(filters: FinanceSummaryFilters): Record<string, string> {
|
||||||
|
const params: Record<string, string> = { dateFrom: filters.dateFrom, dateTo: filters.dateTo };
|
||||||
|
if (filters.granularity) params.granularity = filters.granularity;
|
||||||
|
if (filters.originStationId) params.originStationId = filters.originStationId;
|
||||||
|
if (filters.destinationStationId) params.destinationStationId = filters.destinationStationId;
|
||||||
|
if (filters.method) params.method = filters.method;
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const financeApi = {
|
||||||
|
getSummary: (filters: FinanceSummaryFilters) =>
|
||||||
|
apiClient.get<FinanceSummaryReport>('/reports/finance', { params: toParams(filters) }),
|
||||||
|
|
||||||
|
/** CSV export. `getRaw` because the endpoint streams a bare CSV body, not the `{success,data}` envelope. */
|
||||||
|
exportCsv: (filters: FinanceSummaryFilters) =>
|
||||||
|
apiClient.getRaw<string>('/reports/finance/export', { params: toParams(filters) }),
|
||||||
|
};
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import type { FinanceGranularity, FinanceSummaryReport } from '@/lib/api/finance';
|
||||||
|
|
||||||
|
// Brand palette — rgb(20,113,76), the same green used by ActionButton's primary variant,
|
||||||
|
// so the exported file reads as the same product as the on-screen report.
|
||||||
|
const BRAND = 'FF14714C';
|
||||||
|
const BRAND_DARK = 'FF0E5A3D';
|
||||||
|
const BRAND_TINT = 'FFEAF5EF';
|
||||||
|
const INK = 'FF1F2937';
|
||||||
|
const MUTED = 'FF6B7280';
|
||||||
|
const ROW_ALT = 'FFF7F8F7';
|
||||||
|
const BORDER = 'FFE2E5E1';
|
||||||
|
const WHITE = 'FFFFFFFF';
|
||||||
|
|
||||||
|
const CURRENCY_FMT = '"ETB" #,##0.00';
|
||||||
|
const THIN_BORDER: Partial<ExcelJS.Borders> = {
|
||||||
|
top: { style: 'thin', color: { argb: BORDER } },
|
||||||
|
left: { style: 'thin', color: { argb: BORDER } },
|
||||||
|
bottom: { style: 'thin', color: { argb: BORDER } },
|
||||||
|
right: { style: 'thin', color: { argb: BORDER } },
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ChartImage {
|
||||||
|
dataUrl: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinanceWorkbookInput {
|
||||||
|
report: FinanceSummaryReport;
|
||||||
|
filters: { dateFrom: string; dateTo: string; granularity: FinanceGranularity; originLabel: string; destinationLabel: string; methodLabel: string };
|
||||||
|
methodLabel: (method: string) => string;
|
||||||
|
periodLabel: (period: string, granularity: FinanceGranularity) => string;
|
||||||
|
images: { trend?: ChartImage; segment?: ChartImage; method?: ChartImage };
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleHeaderCell(cell: ExcelJS.Cell) {
|
||||||
|
cell.font = { bold: true, color: { argb: WHITE }, size: 11 };
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND } };
|
||||||
|
cell.alignment = { vertical: 'middle', horizontal: 'left' };
|
||||||
|
cell.border = THIN_BORDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTableHeader(ws: ExcelJS.Worksheet, rowIndex: number, headers: string[], alignRight: Set<number> = new Set()) {
|
||||||
|
const row = ws.getRow(rowIndex);
|
||||||
|
headers.forEach((h, i) => {
|
||||||
|
const cell = row.getCell(i + 1);
|
||||||
|
cell.value = h;
|
||||||
|
styleHeaderCell(cell);
|
||||||
|
if (alignRight.has(i)) cell.alignment = { vertical: 'middle', horizontal: 'right' };
|
||||||
|
});
|
||||||
|
row.height = 20;
|
||||||
|
row.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bandRow(ws: ExcelJS.Worksheet, rowIndex: number, colCount: number, isAlt: boolean) {
|
||||||
|
const row = ws.getRow(rowIndex);
|
||||||
|
for (let c = 1; c <= colCount; c++) {
|
||||||
|
const cell = row.getCell(c);
|
||||||
|
cell.border = THIN_BORDER;
|
||||||
|
if (isAlt) cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: ROW_ALT } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleBanner(ws: ExcelJS.Worksheet, title: string, subtitle: string, colSpan: number) {
|
||||||
|
ws.mergeCells(1, 1, 1, colSpan);
|
||||||
|
const titleCell = ws.getCell(1, 1);
|
||||||
|
titleCell.value = title;
|
||||||
|
titleCell.font = { bold: true, size: 18, color: { argb: WHITE } };
|
||||||
|
titleCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND } };
|
||||||
|
titleCell.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
||||||
|
ws.getRow(1).height = 34;
|
||||||
|
for (let c = 1; c <= colSpan; c++) ws.getCell(1, c).fill = titleCell.fill;
|
||||||
|
|
||||||
|
ws.mergeCells(2, 1, 2, colSpan);
|
||||||
|
const subCell = ws.getCell(2, 1);
|
||||||
|
subCell.value = subtitle;
|
||||||
|
subCell.font = { italic: true, size: 10, color: { argb: MUTED } };
|
||||||
|
subCell.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
||||||
|
ws.getRow(2).height = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
function kpiCard(ws: ExcelJS.Worksheet, startRow: number, startCol: number, span: number, label: string, value: string, accent: string) {
|
||||||
|
ws.mergeCells(startRow, startCol, startRow, startCol + span - 1);
|
||||||
|
ws.mergeCells(startRow + 1, startCol, startRow + 1, startCol + span - 1);
|
||||||
|
|
||||||
|
const labelCell = ws.getCell(startRow, startCol);
|
||||||
|
labelCell.value = label.toUpperCase();
|
||||||
|
labelCell.font = { bold: true, size: 9, color: { argb: MUTED } };
|
||||||
|
labelCell.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
||||||
|
|
||||||
|
const valueCell = ws.getCell(startRow + 1, startCol);
|
||||||
|
valueCell.value = value;
|
||||||
|
valueCell.font = { bold: true, size: 16, color: { argb: accent } };
|
||||||
|
valueCell.alignment = { vertical: 'middle', horizontal: 'left', indent: 1 };
|
||||||
|
|
||||||
|
for (let r = startRow; r <= startRow + 1; r++) {
|
||||||
|
for (let c = startCol; c < startCol + span; c++) {
|
||||||
|
const cell = ws.getCell(r, c);
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND_TINT } };
|
||||||
|
cell.border = {
|
||||||
|
top: r === startRow ? { style: 'thin', color: { argb: BORDER } } : undefined,
|
||||||
|
bottom: r === startRow + 1 ? { style: 'thin', color: { argb: BORDER } } : undefined,
|
||||||
|
left: c === startCol ? { style: 'thin', color: { argb: BORDER } } : undefined,
|
||||||
|
right: c === startCol + span - 1 ? { style: 'thin', color: { argb: BORDER } } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.getRow(startRow).height = 16;
|
||||||
|
ws.getRow(startRow + 1).height = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addImage(wb: ExcelJS.Workbook, ws: ExcelJS.Worksheet, image: ChartImage | undefined, anchorRow: number, heading: string) {
|
||||||
|
const headingCell = ws.getCell(anchorRow, 1);
|
||||||
|
headingCell.value = heading;
|
||||||
|
headingCell.font = { bold: true, size: 12, color: { argb: INK } };
|
||||||
|
ws.getRow(anchorRow).height = 20;
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
const emptyCell = ws.getCell(anchorRow + 1, 1);
|
||||||
|
emptyCell.value = 'No chart available for the current filters.';
|
||||||
|
emptyCell.font = { italic: true, size: 10, color: { argb: MUTED } };
|
||||||
|
return anchorRow + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = 640;
|
||||||
|
const scale = image.width > maxWidth ? maxWidth / image.width : 1;
|
||||||
|
const width = Math.round(image.width * scale);
|
||||||
|
const height = Math.round(image.height * scale);
|
||||||
|
|
||||||
|
const imageId = wb.addImage({ base64: image.dataUrl, extension: 'png' });
|
||||||
|
ws.addImage(imageId, {
|
||||||
|
tl: { col: 0.15, row: anchorRow + 0.15 },
|
||||||
|
ext: { width, height },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance past the image height (≈20px per row) plus a spacer row.
|
||||||
|
const rowsUsed = Math.ceil(height / 20) + 2;
|
||||||
|
return anchorRow + rowsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildFinanceWorkbook(input: FinanceWorkbookInput): Promise<Blob> {
|
||||||
|
const { report, filters, images } = input;
|
||||||
|
const methodLabel = input.methodLabel;
|
||||||
|
const periodLabel = input.periodLabel;
|
||||||
|
|
||||||
|
const wb = new ExcelJS.Workbook();
|
||||||
|
wb.creator = 'EDR Passenger Backoffice';
|
||||||
|
wb.created = new Date();
|
||||||
|
|
||||||
|
// ── Summary sheet ─────────────────────────────────────────────────────────
|
||||||
|
const summary = wb.addWorksheet('Summary', { views: [{ showGridLines: false }] });
|
||||||
|
summary.columns = [{ width: 16 }, { width: 16 }, { width: 16 }, { width: 16 }, { width: 16 }, { width: 16 }];
|
||||||
|
|
||||||
|
titleBanner(
|
||||||
|
summary,
|
||||||
|
'EDR Passenger — Finance Summary',
|
||||||
|
`${filters.dateFrom} to ${filters.dateTo} · ${filters.granularity} · Origin: ${filters.originLabel} · Destination: ${filters.destinationLabel} · Method: ${filters.methodLabel} · Generated ${new Date().toLocaleString('en-US')}`,
|
||||||
|
6,
|
||||||
|
);
|
||||||
|
|
||||||
|
const avgPerBooking = report.totals.bookingCount > 0 ? report.totals.revenueEtbMinor / report.totals.bookingCount : 0;
|
||||||
|
kpiCard(summary, 4, 1, 2, 'Total Revenue', `ETB ${(report.totals.revenueEtbMinor / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`, BRAND_DARK);
|
||||||
|
kpiCard(summary, 4, 3, 2, 'Bookings', report.totals.bookingCount.toLocaleString('en-US'), INK);
|
||||||
|
kpiCard(summary, 4, 5, 2, 'Avg. per Booking', `ETB ${(avgPerBooking / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`, INK);
|
||||||
|
|
||||||
|
let cursor = 7;
|
||||||
|
cursor = addImage(wb, summary, images.trend, cursor, 'Revenue Trend') + 1;
|
||||||
|
cursor = addImage(wb, summary, images.segment, cursor, 'Revenue by Segment') + 1;
|
||||||
|
addImage(wb, summary, images.method, cursor, 'Revenue by Payment Method');
|
||||||
|
|
||||||
|
// ── By Period sheet ──────────────────────────────────────────────────────
|
||||||
|
const byPeriod = wb.addWorksheet('By Period', { views: [{ state: 'frozen', ySplit: 1 }] });
|
||||||
|
byPeriod.columns = [{ width: 18 }, { width: 14 }, { width: 20 }];
|
||||||
|
addTableHeader(byPeriod, 1, ['Period', 'Bookings', 'Revenue (ETB)'], new Set([1, 2]));
|
||||||
|
const periodRows = [...report.byPeriod].sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
periodRows.forEach((p, i) => {
|
||||||
|
const r = byPeriod.getRow(i + 2);
|
||||||
|
r.getCell(1).value = periodLabel(p.key, report.granularity);
|
||||||
|
r.getCell(2).value = p.bookingCount;
|
||||||
|
r.getCell(2).alignment = { horizontal: 'right' };
|
||||||
|
r.getCell(3).value = p.revenueEtbMinor / 100;
|
||||||
|
r.getCell(3).numFmt = CURRENCY_FMT;
|
||||||
|
r.getCell(3).alignment = { horizontal: 'right' };
|
||||||
|
bandRow(byPeriod, i + 2, 3, i % 2 === 1);
|
||||||
|
});
|
||||||
|
byPeriod.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: 3 } };
|
||||||
|
|
||||||
|
// ── By Segment sheet ─────────────────────────────────────────────────────
|
||||||
|
const bySegment = wb.addWorksheet('By Segment', { views: [{ state: 'frozen', ySplit: 1 }] });
|
||||||
|
bySegment.columns = [{ width: 34 }, { width: 14 }, { width: 20 }];
|
||||||
|
addTableHeader(bySegment, 1, ['Origin → Destination', 'Bookings', 'Revenue (ETB)'], new Set([1, 2]));
|
||||||
|
report.bySegment.forEach((s, i) => {
|
||||||
|
const r = bySegment.getRow(i + 2);
|
||||||
|
r.getCell(1).value = s.label;
|
||||||
|
r.getCell(2).value = s.bookingCount;
|
||||||
|
r.getCell(2).alignment = { horizontal: 'right' };
|
||||||
|
r.getCell(3).value = s.revenueEtbMinor / 100;
|
||||||
|
r.getCell(3).numFmt = CURRENCY_FMT;
|
||||||
|
r.getCell(3).alignment = { horizontal: 'right' };
|
||||||
|
bandRow(bySegment, i + 2, 3, i % 2 === 1);
|
||||||
|
});
|
||||||
|
bySegment.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: 3 } };
|
||||||
|
|
||||||
|
// ── By Method sheet ──────────────────────────────────────────────────────
|
||||||
|
const byMethod = wb.addWorksheet('By Method', { views: [{ state: 'frozen', ySplit: 1 }] });
|
||||||
|
byMethod.columns = [{ width: 20 }, { width: 14 }, { width: 20 }, { width: 12 }];
|
||||||
|
addTableHeader(byMethod, 1, ['Payment Method', 'Bookings', 'Revenue (ETB)', 'Share'], new Set([1, 2, 3]));
|
||||||
|
const methodTotal = report.byMethod.reduce((sum, m) => sum + m.revenueEtbMinor, 0);
|
||||||
|
report.byMethod.forEach((m, i) => {
|
||||||
|
const r = byMethod.getRow(i + 2);
|
||||||
|
r.getCell(1).value = methodLabel(m.key);
|
||||||
|
r.getCell(2).value = m.bookingCount;
|
||||||
|
r.getCell(2).alignment = { horizontal: 'right' };
|
||||||
|
r.getCell(3).value = m.revenueEtbMinor / 100;
|
||||||
|
r.getCell(3).numFmt = CURRENCY_FMT;
|
||||||
|
r.getCell(3).alignment = { horizontal: 'right' };
|
||||||
|
r.getCell(4).value = methodTotal > 0 ? m.revenueEtbMinor / methodTotal : 0;
|
||||||
|
r.getCell(4).numFmt = '0.0%';
|
||||||
|
r.getCell(4).alignment = { horizontal: 'right' };
|
||||||
|
bandRow(byMethod, i + 2, 4, i % 2 === 1);
|
||||||
|
});
|
||||||
|
byMethod.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: 4 } };
|
||||||
|
|
||||||
|
// ── Detail sheet — every row, unpaginated ───────────────────────────────
|
||||||
|
const detail = wb.addWorksheet('Detail', { views: [{ state: 'frozen', ySplit: 1 }] });
|
||||||
|
detail.columns = [{ width: 18 }, { width: 34 }, { width: 18 }, { width: 14 }, { width: 20 }];
|
||||||
|
addTableHeader(detail, 1, ['Period', 'Origin → Destination', 'Payment Method', 'Bookings', 'Revenue (ETB)'], new Set([2, 3]));
|
||||||
|
report.rows.forEach((row, i) => {
|
||||||
|
const r = detail.getRow(i + 2);
|
||||||
|
r.getCell(1).value = periodLabel(row.period, report.granularity);
|
||||||
|
r.getCell(2).value = row.segmentLabel;
|
||||||
|
r.getCell(3).value = methodLabel(row.method);
|
||||||
|
r.getCell(4).value = row.bookingCount;
|
||||||
|
r.getCell(4).alignment = { horizontal: 'right' };
|
||||||
|
r.getCell(5).value = row.revenueEtbMinor / 100;
|
||||||
|
r.getCell(5).numFmt = CURRENCY_FMT;
|
||||||
|
r.getCell(5).alignment = { horizontal: 'right' };
|
||||||
|
bandRow(detail, i + 2, 5, i % 2 === 1);
|
||||||
|
});
|
||||||
|
detail.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: 5 } };
|
||||||
|
|
||||||
|
const buffer = await wb.xlsx.writeBuffer();
|
||||||
|
return new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||||
|
}
|
||||||
@@ -68,6 +68,24 @@
|
|||||||
.animate-fade-up {
|
.animate-fade-up {
|
||||||
animation: fade-up 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
|
animation: fade-up 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
from { background-position: -300px 0; }
|
||||||
|
to { background-position: 300px 0; }
|
||||||
|
}
|
||||||
|
.skeleton {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: hsl(var(--muted));
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
hsl(var(--muted)) 0%,
|
||||||
|
hsl(var(--muted-foreground) / 0.18) 50%,
|
||||||
|
hsl(var(--muted)) 100%
|
||||||
|
);
|
||||||
|
background-size: 600px 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation: shimmer 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|||||||
293
pnpm-lock.yaml
generated
293
pnpm-lock.yaml
generated
@@ -604,7 +604,7 @@ importers:
|
|||||||
version: 5.101.0(react@19.2.6)
|
version: 5.101.0(react@19.2.6)
|
||||||
'@tria-plc/iamui':
|
'@tria-plc/iamui':
|
||||||
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
|
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
|
||||||
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)
|
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(0ce39b7e349029277dcd938d06eeb0f7)
|
||||||
'@vis.gl/react-google-maps':
|
'@vis.gl/react-google-maps':
|
||||||
specifier: ^1.8.3
|
specifier: ^1.8.3
|
||||||
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -983,6 +983,12 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
exceljs:
|
||||||
|
specifier: ^4.4.0
|
||||||
|
version: 4.4.0
|
||||||
|
html-to-image:
|
||||||
|
specifier: ^1.11.11
|
||||||
|
version: 1.11.13
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.446.0
|
specifier: ^0.446.0
|
||||||
version: 0.446.0(react@18.3.1)
|
version: 0.446.0(react@18.3.1)
|
||||||
@@ -8338,6 +8344,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==}
|
resolution: {integrity: sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
html-to-image@1.11.13:
|
||||||
|
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||||
|
|
||||||
html-url-attributes@3.0.1:
|
html-url-attributes@3.0.1:
|
||||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
@@ -13063,11 +13072,11 @@ snapshots:
|
|||||||
'@babel/helpers': 7.29.7
|
'@babel/helpers': 7.29.7
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -13102,7 +13111,7 @@ snapshots:
|
|||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13111,14 +13120,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/traverse': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13133,9 +13135,9 @@ snapshots:
|
|||||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -13150,13 +13152,13 @@ snapshots:
|
|||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13309,18 +13311,6 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/traverse@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.29.7
|
|
||||||
'@babel/generator': 7.29.7
|
|
||||||
'@babel/helper-globals': 7.29.7
|
|
||||||
'@babel/parser': 7.29.7
|
|
||||||
'@babel/template': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/traverse@7.29.7(supports-color@5.5.0)':
|
'@babel/traverse@7.29.7(supports-color@5.5.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
@@ -13805,7 +13795,7 @@ snapshots:
|
|||||||
|
|
||||||
'@emotion/babel-plugin@11.13.5':
|
'@emotion/babel-plugin@11.13.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
'@emotion/hash': 0.9.2
|
'@emotion/hash': 0.9.2
|
||||||
'@emotion/memoize': 0.9.0
|
'@emotion/memoize': 0.9.0
|
||||||
@@ -13971,7 +13961,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -14131,7 +14121,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -15730,7 +15720,7 @@ snapshots:
|
|||||||
|
|
||||||
'@puppeteer/browsers@2.13.2':
|
'@puppeteer/browsers@2.13.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
extract-zip: 2.0.1
|
extract-zip: 2.0.1
|
||||||
progress: 2.0.3
|
progress: 2.0.3
|
||||||
proxy-agent: 6.5.0
|
proxy-agent: 6.5.0
|
||||||
@@ -17804,7 +17794,7 @@ snapshots:
|
|||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
token-types: 6.1.2
|
token-types: 6.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -18103,130 +18093,6 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
'@tria-plc/iamui@file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)':
|
|
||||||
dependencies:
|
|
||||||
'@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@hookform/resolvers': 5.4.0(react-hook-form@7.77.0(react@19.2.6))
|
|
||||||
'@lottiefiles/react-lottie-player': 3.6.0(react@19.2.6)
|
|
||||||
'@mantine/charts': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1))
|
|
||||||
'@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@mantine/hooks': 7.17.8(react@19.2.6)
|
|
||||||
'@mantine/notifications': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-accordion': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-avatar': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-checkbox': 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-collapsible': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-context-menu': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-hover-card': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-label': 2.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-popover': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-progress': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-radio-group': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-select': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-separator': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-slot': 1.2.5(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@radix-ui/react-switch': 1.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-tabs': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-toast': 1.2.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-tooltip': 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf-viewer/core': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf-viewer/default-layout': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf/renderer': 4.5.1(react@19.2.6)
|
|
||||||
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
|
|
||||||
'@tabler/icons-react': 3.44.0(react@19.2.6)
|
|
||||||
'@tailwindcss/vite': 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0))
|
|
||||||
'@tanstack/react-query': 5.101.0(react@19.2.6)
|
|
||||||
'@tanstack/react-query-devtools': 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6)
|
|
||||||
'@tanstack/react-table': 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@tinymce/tinymce-react': 6.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tinymce@7.9.3)
|
|
||||||
'@types/dompurify': 3.2.0
|
|
||||||
'@types/node': 24.13.1
|
|
||||||
'@types/tinymce': 4.6.9
|
|
||||||
axios: 1.17.0
|
|
||||||
class-variance-authority: 0.7.1
|
|
||||||
clsx: 2.1.1
|
|
||||||
cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
date-fns: 3.6.0
|
|
||||||
dayjs: 1.11.21
|
|
||||||
dompurify: 3.4.8
|
|
||||||
ethiopian-calendar-date-converter: 2.1.6
|
|
||||||
ethiopian-calendar-new: 1.1.0
|
|
||||||
file-type: 18.7.0
|
|
||||||
framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
html2canvas: 1.4.1
|
|
||||||
i18next: 25.10.10(typescript@5.9.3)
|
|
||||||
i18next-browser-languagedetector: 8.2.1
|
|
||||||
jquery: 3.7.1
|
|
||||||
js-cookie: 3.0.8
|
|
||||||
jspdf: 3.0.4
|
|
||||||
lodash: 4.18.1
|
|
||||||
lucide-react: 0.513.0(react@19.2.6)
|
|
||||||
mantine-react-table: 2.0.0-beta.9(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
mui-ethiopian-datepicker: 0.3.2(4b3af212eafdf0059f009b005d7e343d)
|
|
||||||
next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
path: 0.12.7
|
|
||||||
pdf-lib: 1.17.1
|
|
||||||
qs: 6.15.2
|
|
||||||
react: 19.2.6
|
|
||||||
react-cookie: 8.1.2(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
react-css-nocode-editor: 1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
react-day-picker: 8.10.2(date-fns@3.6.0)(react@19.2.6)
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
react-dropzone: 14.4.1(react@19.2.6)
|
|
||||||
react-hook-form: 7.77.0(react@19.2.6)
|
|
||||||
react-i18next: 15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)
|
|
||||||
react-icons: 5.6.0(react@19.2.6)
|
|
||||||
react-image-crop: 11.0.10(react@19.2.6)
|
|
||||||
react-intersection-observer: 9.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-pdf: 10.4.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-pdf-html: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6)
|
|
||||||
react-redux: 9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1)
|
|
||||||
react-resizable-panels: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-router-dom: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-signature-canvas: 1.1.0-alpha.2(@types/prop-types@15.7.15)(@types/react@18.3.31)(prop-types@15.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
recharts: 3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1)
|
|
||||||
rollup-plugin-visualizer: 7.0.1(rollup@4.61.1)
|
|
||||||
socket.io-client: 4.8.3
|
|
||||||
sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
tailwind-merge: 3.6.0
|
|
||||||
tailwind-scrollbar-hide: 4.0.0(tailwindcss@4.3.0)
|
|
||||||
tailwindcss: 4.3.0
|
|
||||||
tailwindcss-animate: 1.0.7(tailwindcss@4.3.0)
|
|
||||||
tinymce: 7.9.3
|
|
||||||
url: 0.11.4
|
|
||||||
vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
xlsx: 0.18.5
|
|
||||||
zod: 3.25.76
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- '@emotion/is-prop-valid'
|
|
||||||
- '@mui/icons-material'
|
|
||||||
- '@mui/material'
|
|
||||||
- '@mui/x-date-pickers'
|
|
||||||
- '@types/prop-types'
|
|
||||||
- '@types/react'
|
|
||||||
- '@types/react-dom'
|
|
||||||
- bufferutil
|
|
||||||
- debug
|
|
||||||
- pdfjs-dist
|
|
||||||
- prop-types
|
|
||||||
- react-is
|
|
||||||
- react-native
|
|
||||||
- redux
|
|
||||||
- rolldown
|
|
||||||
- rollup
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
- utf-8-validate
|
|
||||||
- vite
|
|
||||||
|
|
||||||
'@ts-morph/common@0.27.0':
|
'@ts-morph/common@0.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
@@ -18624,7 +18490,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.60.1
|
'@typescript-eslint/visitor-keys': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -18634,7 +18500,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -18653,7 +18519,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -18668,7 +18534,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/visitor-keys': 8.60.1
|
'@typescript-eslint/visitor-keys': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.2
|
semver: 7.8.2
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
@@ -18957,7 +18823,7 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -19471,16 +19337,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-plugin-styled-components@2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0):
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
|
||||||
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
|
||||||
picomatch: 4.0.4
|
|
||||||
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
babel-polyfill@6.26.0:
|
babel-polyfill@6.26.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime: 6.26.0
|
babel-runtime: 6.26.0
|
||||||
@@ -19636,7 +19492,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
http-errors: 2.0.1
|
http-errors: 2.0.1
|
||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -20685,7 +20541,7 @@ snapshots:
|
|||||||
engine.io-client@6.6.5:
|
engine.io-client@6.6.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-parser: 5.2.3
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.20.1
|
ws: 8.20.1
|
||||||
xmlhttprequest-ssl: 2.1.2
|
xmlhttprequest-ssl: 2.1.2
|
||||||
@@ -20705,7 +20561,7 @@ snapshots:
|
|||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cors: 2.8.6
|
cors: 2.8.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-parser: 5.2.3
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -20936,7 +20792,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.0
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@@ -21064,7 +20920,7 @@ snapshots:
|
|||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -21301,7 +21157,7 @@ snapshots:
|
|||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cookie-signature: 1.2.2
|
cookie-signature: 1.2.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
@@ -21354,7 +21210,7 @@ snapshots:
|
|||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
get-stream: 5.2.0
|
get-stream: 5.2.0
|
||||||
yauzl: 2.10.0
|
yauzl: 2.10.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -21509,7 +21365,7 @@ snapshots:
|
|||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -21757,7 +21613,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
basic-ftp: 5.3.1
|
basic-ftp: 5.3.1
|
||||||
data-uri-to-buffer: 6.0.2
|
data-uri-to-buffer: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -22046,6 +21902,8 @@ snapshots:
|
|||||||
is-self-closing: 1.0.1
|
is-self-closing: 1.0.1
|
||||||
kind-of: 6.0.3
|
kind-of: 6.0.3
|
||||||
|
|
||||||
|
html-to-image@1.11.13: {}
|
||||||
|
|
||||||
html-url-attributes@3.0.1: {}
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
html2canvas@1.4.1:
|
html2canvas@1.4.1:
|
||||||
@@ -22064,7 +21922,7 @@ snapshots:
|
|||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -22077,14 +21935,14 @@ snapshots:
|
|||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -22524,7 +22382,7 @@ snapshots:
|
|||||||
|
|
||||||
istanbul-lib-source-maps@4.0.1:
|
istanbul-lib-source-maps@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -23184,7 +23042,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
commander: 13.1.0
|
commander: 13.1.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
execa: 8.0.1
|
execa: 8.0.1
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
listr2: 8.3.3
|
listr2: 8.3.3
|
||||||
@@ -23871,7 +23729,7 @@ snapshots:
|
|||||||
micromark@4.0.2:
|
micromark@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/debug': 4.1.13
|
'@types/debug': 4.1.13
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
decode-named-character-reference: 1.3.0
|
decode-named-character-reference: 1.3.0
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
micromark-core-commonmark: 2.0.3
|
micromark-core-commonmark: 2.0.3
|
||||||
@@ -24397,7 +24255,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
get-uri: 6.0.5
|
get-uri: 6.0.5
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
@@ -24747,7 +24605,7 @@ snapshots:
|
|||||||
proxy-agent@6.5.0:
|
proxy-agent@6.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
lru-cache: 7.18.3
|
lru-cache: 7.18.3
|
||||||
@@ -24776,7 +24634,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@puppeteer/browsers': 2.13.2
|
'@puppeteer/browsers': 2.13.2
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
|
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
devtools-protocol: 0.0.1608973
|
devtools-protocol: 0.0.1608973
|
||||||
typed-query-selector: 2.12.2
|
typed-query-selector: 2.12.2
|
||||||
webdriver-bidi-protocol: 0.4.1
|
webdriver-bidi-protocol: 0.4.1
|
||||||
@@ -25029,15 +24887,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- react-is
|
- react-is
|
||||||
|
|
||||||
react-css-nocode-editor@1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.6
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- react-is
|
|
||||||
|
|
||||||
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
|
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 3.6.0
|
date-fns: 3.6.0
|
||||||
@@ -25648,7 +25497,7 @@ snapshots:
|
|||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
is-promise: 4.0.0
|
is-promise: 4.0.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
@@ -25770,7 +25619,7 @@ snapshots:
|
|||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
@@ -25986,7 +25835,7 @@ snapshots:
|
|||||||
|
|
||||||
socket.io-adapter@2.5.8:
|
socket.io-adapter@2.5.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -25996,7 +25845,7 @@ snapshots:
|
|||||||
socket.io-client@4.8.3:
|
socket.io-client@4.8.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-client: 6.6.5
|
engine.io-client: 6.6.5
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -26007,7 +25856,7 @@ snapshots:
|
|||||||
socket.io-parser@4.2.6:
|
socket.io-parser@4.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -26016,7 +25865,7 @@ snapshots:
|
|||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cors: 2.8.6
|
cors: 2.8.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io: 6.6.9
|
engine.io: 6.6.9
|
||||||
socket.io-adapter: 2.5.8
|
socket.io-adapter: 2.5.8
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
@@ -26028,7 +25877,7 @@ snapshots:
|
|||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
socks: 2.8.9
|
socks: 2.8.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -26323,24 +26172,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
|
|
||||||
styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@emotion/is-prop-valid': 1.4.0
|
|
||||||
'@emotion/stylis': 0.8.5
|
|
||||||
'@emotion/unitless': 0.7.5
|
|
||||||
babel-plugin-styled-components: 2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0)
|
|
||||||
css-to-react-native: 3.2.0
|
|
||||||
hoist-non-react-statics: 3.3.2
|
|
||||||
react: 19.2.6
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
react-is: 19.2.7
|
|
||||||
shallowequal: 1.1.0
|
|
||||||
supports-color: 5.5.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
|
|
||||||
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
|
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
@@ -26366,7 +26197,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
component-emitter: 1.3.1
|
component-emitter: 1.3.1
|
||||||
cookiejar: 2.1.4
|
cookiejar: 2.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
formidable: 3.5.4
|
formidable: 3.5.4
|
||||||
@@ -26879,7 +26710,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
dayjs: 1.11.21
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -26903,7 +26734,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
dayjs: 1.11.21
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -27264,7 +27095,7 @@ snapshots:
|
|||||||
vite-node@2.1.9(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0):
|
vite-node@2.1.9(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0)
|
||||||
@@ -27282,7 +27113,7 @@ snapshots:
|
|||||||
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
||||||
@@ -27329,7 +27160,7 @@ snapshots:
|
|||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
@@ -27365,7 +27196,7 @@ snapshots:
|
|||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user