Fix calendar drag snap-back, IG filter, and default to 2026.
Prefer the latest year in the archive, normalize Instagram casing so stories show under IG/FB, sync pipeline dates on move, and stop stale plan re-renders from undoing a successful drag. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,7 +15,7 @@ BASE = "https://dokploy.4ventures.es"
|
|||||||
APP_ID = "NZS797t9Rnq9Pj4mHiii3"
|
APP_ID = "NZS797t9Rnq9Pj4mHiii3"
|
||||||
SCHEDULE_NAME = "pipeline-publish"
|
SCHEDULE_NAME = "pipeline-publish"
|
||||||
CRON_EXPR = "*/5 * * * *"
|
CRON_EXPR = "*/5 * * * *"
|
||||||
# Public URL + env expansion via bash -lc so CRON_SECRET from app env is used.
|
# Runs inside the app container so $CRON_SECRET from Dokploy env is available.
|
||||||
CMD = (
|
CMD = (
|
||||||
"bash -lc "
|
"bash -lc "
|
||||||
"'curl -sS -H \"Authorization: Bearer $CRON_SECRET\" "
|
"'curl -sS -H \"Authorization: Bearer $CRON_SECRET\" "
|
||||||
@@ -71,39 +71,51 @@ def main() -> int:
|
|||||||
try { json = JSON.parse(text); } catch {}
|
try { json = JSON.parse(text); } catch {}
|
||||||
return { ok: r.ok, status: r.status, text: text.slice(0, 1200), json };
|
return { ok: r.ok, status: r.status, text: text.slice(0, 1200), json };
|
||||||
};
|
};
|
||||||
|
const get = async (path) => {
|
||||||
|
const r = await fetch(path, { credentials: 'include' });
|
||||||
|
const text = await r.text();
|
||||||
|
let json = null;
|
||||||
|
try { json = JSON.parse(text); } catch {}
|
||||||
|
return { ok: r.ok, status: r.status, text: text.slice(0, 2000), json };
|
||||||
|
};
|
||||||
|
|
||||||
// Delete existing schedules with same name
|
// List application schedules (Dokploy v0.30)
|
||||||
for (const listPath of [
|
const listed = await get(
|
||||||
'/api/schedule.all',
|
'/api/trpc/schedule.list?batch=1&input=' +
|
||||||
'/api/schedule.byApplicationId',
|
encodeURIComponent(JSON.stringify({
|
||||||
]) {
|
'0': { json: { id: appId, scheduleType: 'application' } }
|
||||||
const listed = await post(listPath, { applicationId: appId });
|
}))
|
||||||
const rows = Array.isArray(listed.json)
|
);
|
||||||
? listed.json
|
const rows =
|
||||||
: listed.json?.data || listed.json?.schedules || [];
|
listed.json?.[0]?.result?.data?.json ||
|
||||||
if (Array.isArray(rows)) {
|
listed.json?.[0]?.result?.data ||
|
||||||
for (const row of rows) {
|
[];
|
||||||
const n = row?.name || row?.scheduleName;
|
const existing = Array.isArray(rows)
|
||||||
const id = row?.scheduleId || row?.id;
|
? rows.filter((r) => (r?.name || '') === name)
|
||||||
if (n === name && id) {
|
: [];
|
||||||
await post('/api/schedule.delete', { scheduleId: id });
|
|
||||||
await post('/api/schedule.remove', { scheduleId: id });
|
for (const row of existing) {
|
||||||
}
|
const sid = row.scheduleId || row.id;
|
||||||
}
|
if (!sid) continue;
|
||||||
}
|
await post('/api/trpc/schedule.delete?batch=1', {
|
||||||
|
'0': { json: { scheduleId: sid } },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await post('/api/schedule.create', {
|
const created = await post('/api/trpc/schedule.create?batch=1', {
|
||||||
applicationId: appId,
|
'0': {
|
||||||
name,
|
json: {
|
||||||
cron,
|
name,
|
||||||
cronExpression: cron,
|
cronExpression: cron,
|
||||||
command,
|
shellType: 'bash',
|
||||||
shellType: 'bash',
|
command,
|
||||||
enabled: true,
|
applicationId: appId,
|
||||||
scheduleType: 'application',
|
scheduleType: 'application',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return { created };
|
return { deleted: existing.length, created };
|
||||||
}""",
|
}""",
|
||||||
{
|
{
|
||||||
"appId": APP_ID,
|
"appId": APP_ID,
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||||
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
import {
|
||||||
|
isAgencyRole,
|
||||||
|
serviceHeaders,
|
||||||
|
supabaseUrl,
|
||||||
|
userRole,
|
||||||
|
} from '@/lib/agency';
|
||||||
import type { RrssPiece } from '@/components/RrssMonthView';
|
import type { RrssPiece } from '@/components/RrssMonthView';
|
||||||
|
import { emptyMonthPlan } from '@/lib/calendar-documents';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -30,6 +36,81 @@ function piecesOf(content: Record<string, unknown>): RrssPiece[] {
|
|||||||
return Array.isArray(content.pieces) ? (content.pieces as RrssPiece[]) : [];
|
return Array.isArray(content.pieces) ? (content.pieces as RrssPiece[]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function monthTitle(ym: string): string {
|
||||||
|
const labels = [
|
||||||
|
'Enero',
|
||||||
|
'Febrero',
|
||||||
|
'Marzo',
|
||||||
|
'Abril',
|
||||||
|
'Mayo',
|
||||||
|
'Junio',
|
||||||
|
'Julio',
|
||||||
|
'Agosto',
|
||||||
|
'Septiembre',
|
||||||
|
'Octubre',
|
||||||
|
'Noviembre',
|
||||||
|
'Diciembre',
|
||||||
|
];
|
||||||
|
const m = Number(ym.slice(5, 7)) - 1;
|
||||||
|
return `RRSS ${labels[m] || ym} ${ym.slice(0, 4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patchDoc(
|
||||||
|
base: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
id: string,
|
||||||
|
plan: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
return fetch(`${base}/rest/v1/documents?id=eq.${encodeURIComponent(id)}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Prefer: 'return=minimal',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: plan,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep creative_pipeline.scheduled_at in sync so overlays don't snap back. */
|
||||||
|
async function syncPipelineSchedule(
|
||||||
|
base: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
tenantId: string,
|
||||||
|
pieceId: string,
|
||||||
|
date: string,
|
||||||
|
time?: string
|
||||||
|
) {
|
||||||
|
const hhmm = (time || '10:00').slice(0, 5);
|
||||||
|
const scheduledAt = new Date(`${date}T${hhmm}:00`).toISOString();
|
||||||
|
const pipeId = pieceId.startsWith('pipe-') ? pieceId.slice(5) : null;
|
||||||
|
|
||||||
|
const filters = pipeId
|
||||||
|
? `id=eq.${encodeURIComponent(pipeId)}`
|
||||||
|
: `calendar_piece_id=eq.${encodeURIComponent(pieceId)}`;
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
`${base}/rest/v1/creative_pipeline?tenant_id=eq.${encodeURIComponent(
|
||||||
|
tenantId
|
||||||
|
)}&${filters}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Prefer: 'return=minimal',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
scheduled_at: scheduledAt,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function PATCH(req: NextRequest) {
|
export async function PATCH(req: NextRequest) {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const {
|
const {
|
||||||
@@ -38,6 +119,13 @@ export async function PATCH(req: NextRequest) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
const agency = isAgencyRole(userRole(user));
|
||||||
|
if (!agency) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Solo la agencia puede mover piezas' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
const ctx = await resolveActiveTenant(user);
|
const ctx = await resolveActiveTenant(user);
|
||||||
if (!ctx.tenantId || ctx.portfolioWide) {
|
if (!ctx.tenantId || ctx.portfolioWide) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -90,8 +178,24 @@ export async function PATCH(req: NextRequest) {
|
|||||||
const ym = date.slice(0, 7);
|
const ym = date.slice(0, 7);
|
||||||
let targetDoc: PlanDoc | null = source?.doc || null;
|
let targetDoc: PlanDoc | null = source?.doc || null;
|
||||||
let targetPlan: Record<string, unknown> | null = source?.plan || null;
|
let targetPlan: Record<string, unknown> | null = source?.plan || null;
|
||||||
|
const sourceMonth = source ? String(source.plan.month || '') : '';
|
||||||
|
|
||||||
if (!targetDoc || (source && String(source.plan.month || '') !== ym)) {
|
// Same-document move when the piece already lives in the target month doc
|
||||||
|
// (also covers old imports where plan.month was missing).
|
||||||
|
if (
|
||||||
|
source &&
|
||||||
|
(sourceMonth === ym ||
|
||||||
|
(!sourceMonth &&
|
||||||
|
piecesOf(source.plan).some((p) =>
|
||||||
|
String(p.date || '').startsWith(ym)
|
||||||
|
)))
|
||||||
|
) {
|
||||||
|
targetDoc = source.doc;
|
||||||
|
targetPlan = source.plan;
|
||||||
|
if (!targetPlan.month) targetPlan.month = ym;
|
||||||
|
} else if (!targetDoc || sourceMonth !== ym) {
|
||||||
|
targetDoc = null;
|
||||||
|
targetPlan = null;
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const plan = asObject(doc.content);
|
const plan = asObject(doc.content);
|
||||||
if (!plan) continue;
|
if (!plan) continue;
|
||||||
@@ -103,33 +207,71 @@ export async function PATCH(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-create month doc when dragging into a month that has no calendar yet.
|
||||||
if (!targetDoc || !targetPlan) {
|
if (!targetDoc || !targetPlan) {
|
||||||
return NextResponse.json(
|
const plan = emptyMonthPlan(ym, ctx.label || 'Cliente');
|
||||||
{ error: 'No hay un calendario guardado para ese mes.' },
|
const create = await fetch(`${base}/rest/v1/documents`, {
|
||||||
{ status: 404 }
|
method: 'POST',
|
||||||
);
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Prefer: 'return=representation',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tenant_id: ctx.tenantId,
|
||||||
|
type: 'calendar',
|
||||||
|
title: monthTitle(ym),
|
||||||
|
content: plan,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!create.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
'No hay calendario para ese mes y no se pudo crear uno nuevo.',
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const created = (await create.json()) as PlanDoc[];
|
||||||
|
targetDoc = Array.isArray(created) ? created[0] : created;
|
||||||
|
targetPlan = plan as unknown as Record<string, unknown>;
|
||||||
|
if (!targetDoc?.id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No hay un calendario guardado para ese mes.' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetPieces = piecesOf(targetPlan);
|
const targetPieces = piecesOf(targetPlan);
|
||||||
|
const movedTime =
|
||||||
|
source?.plan
|
||||||
|
? piecesOf(source.plan)[source.index]?.time
|
||||||
|
: body?.snapshot?.time;
|
||||||
|
|
||||||
if (source && source.doc.id === targetDoc.id) {
|
if (source && source.doc.id === targetDoc.id) {
|
||||||
targetPieces[source.index] = { ...targetPieces[source.index], date };
|
targetPieces[source.index] = {
|
||||||
|
...targetPieces[source.index],
|
||||||
|
date,
|
||||||
|
platform: String(targetPieces[source.index].platform || 'instagram')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase(),
|
||||||
|
};
|
||||||
} else if (source) {
|
} else if (source) {
|
||||||
const moved = { ...piecesOf(source.plan)[source.index], date };
|
const moved = {
|
||||||
|
...piecesOf(source.plan)[source.index],
|
||||||
|
date,
|
||||||
|
platform: String(
|
||||||
|
piecesOf(source.plan)[source.index]?.platform || 'instagram'
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase(),
|
||||||
|
};
|
||||||
piecesOf(source.plan).splice(source.index, 1);
|
piecesOf(source.plan).splice(source.index, 1);
|
||||||
source.plan.pieces = piecesOf(source.plan);
|
source.plan.pieces = piecesOf(source.plan);
|
||||||
const srcPatch = await fetch(
|
if (!source.plan.month && sourceMonth) source.plan.month = sourceMonth;
|
||||||
`${base}/rest/v1/documents?id=eq.${source.doc.id}`,
|
const srcPatch = await patchDoc(base, headers, source.doc.id, source.plan);
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Prefer: 'return=minimal',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content: source.plan }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!srcPatch.ok) {
|
if (!srcPatch.ok) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'No se pudo actualizar el mes de origen.' },
|
{ error: 'No se pudo actualizar el mes de origen.' },
|
||||||
@@ -138,7 +280,14 @@ export async function PATCH(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
targetPieces.push(moved);
|
targetPieces.push(moved);
|
||||||
} else if (body?.snapshot) {
|
} else if (body?.snapshot) {
|
||||||
targetPieces.push({ ...body.snapshot, date, id: pieceId });
|
targetPieces.push({
|
||||||
|
...body.snapshot,
|
||||||
|
date,
|
||||||
|
id: pieceId,
|
||||||
|
platform: String(body.snapshot.platform || 'instagram')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'No se encontró esa pieza en el calendario.' },
|
{ error: 'No se encontró esa pieza en el calendario.' },
|
||||||
@@ -146,19 +295,12 @@ export async function PATCH(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetPlan.month = ym;
|
||||||
targetPlan.pieces = targetPieces.sort((a, b) =>
|
targetPlan.pieces = targetPieces.sort((a, b) =>
|
||||||
String(a.date).localeCompare(String(b.date))
|
String(a.date).localeCompare(String(b.date))
|
||||||
);
|
);
|
||||||
|
|
||||||
const patch = await fetch(`${base}/rest/v1/documents?id=eq.${targetDoc.id}`, {
|
const patch = await patchDoc(base, headers, targetDoc.id, targetPlan);
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Prefer: 'return=minimal',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content: targetPlan }),
|
|
||||||
});
|
|
||||||
if (!patch.ok) {
|
if (!patch.ok) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: (await patch.text()).slice(0, 240) || 'No se pudo guardar.' },
|
{ error: (await patch.text()).slice(0, 240) || 'No se pudo guardar.' },
|
||||||
@@ -166,5 +308,14 @@ export async function PATCH(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncPipelineSchedule(
|
||||||
|
base,
|
||||||
|
headers,
|
||||||
|
ctx.tenantId,
|
||||||
|
pieceId,
|
||||||
|
date,
|
||||||
|
movedTime
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json({ ok: true, pieceId, date });
|
return NextResponse.json({ ok: true, pieceId, date });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ export default async function CalendarioPage({
|
|||||||
if (!plan) continue;
|
if (!plan) continue;
|
||||||
const blob = JSON.stringify(plan).toLowerCase();
|
const blob = JSON.stringify(plan).toLowerCase();
|
||||||
if (!isAguaMiia && blob.includes('agua miia')) continue;
|
if (!isAguaMiia && blob.includes('agua miia')) continue;
|
||||||
|
// Normalize platform casing so IG/FB filter matches Drive imports ("Instagram").
|
||||||
|
plan.pieces = (plan.pieces || []).map((p) => ({
|
||||||
|
...p,
|
||||||
|
platform: String(p.platform || 'instagram').trim().toLowerCase(),
|
||||||
|
}));
|
||||||
plans.push(plan);
|
plans.push(plan);
|
||||||
}
|
}
|
||||||
if (!plans.length && isAguaMiia) {
|
if (!plans.length && isAguaMiia) {
|
||||||
@@ -124,8 +129,16 @@ export default async function CalendarioPage({
|
|||||||
plans.splice(0, plans.length, ...(merged as RrssMonthPlan[]));
|
plans.splice(0, plans.length, ...(merged as RrssMonthPlan[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthParam = sp.month || plans[0]?.month;
|
// Prefer the latest year/month (e.g. 2026 over 2025) unless ?month= is set.
|
||||||
const active = plans.find((p) => p.month === monthParam) || plans[0];
|
const sortedMonths = [...plans.map((p) => p.month).filter(Boolean)].sort();
|
||||||
|
const latestMonth = sortedMonths[sortedMonths.length - 1] || null;
|
||||||
|
const monthParam = sp.month || latestMonth || plans[0]?.month;
|
||||||
|
const active =
|
||||||
|
plans.find((p) => p.month === monthParam) ||
|
||||||
|
(latestMonth
|
||||||
|
? plans.find((p) => p.month === latestMonth)
|
||||||
|
: undefined) ||
|
||||||
|
plans[0];
|
||||||
// Portada = archivo anual estilo /rrss/ig-fb/
|
// Portada = archivo anual estilo /rrss/ig-fb/
|
||||||
const view = sp.view || 'archive';
|
const view = sp.view || 'archive';
|
||||||
const initialPlatform =
|
const initialPlatform =
|
||||||
@@ -172,7 +185,21 @@ export default async function CalendarioPage({
|
|||||||
|
|
||||||
// Archive of months (like /rrss/ig-fb/)
|
// Archive of months (like /rrss/ig-fb/)
|
||||||
if (view === 'archive') {
|
if (view === 'archive') {
|
||||||
const year = Number((active?.month || '2026-10').slice(0, 4)) || 2026;
|
const years = [
|
||||||
|
...new Set(
|
||||||
|
plans
|
||||||
|
.map((p) => Number(String(p.month || '').slice(0, 4)))
|
||||||
|
.filter((y) => y >= 2000 && y <= 2100)
|
||||||
|
),
|
||||||
|
].sort((a, b) => a - b);
|
||||||
|
const requestedYear = Number(String(monthParam || '').slice(0, 4));
|
||||||
|
const year =
|
||||||
|
(requestedYear >= 2000 && requestedYear <= 2100
|
||||||
|
? requestedYear
|
||||||
|
: null) ||
|
||||||
|
years[years.length - 1] ||
|
||||||
|
Number((active?.month || '2026-10').slice(0, 4)) ||
|
||||||
|
2026;
|
||||||
return (
|
return (
|
||||||
<RrssArchiveYear
|
<RrssArchiveYear
|
||||||
year={year}
|
year={year}
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ export default function RrssArchiveYear({
|
|||||||
const byMonth = new Map(plans.map((p) => [p.month, p]));
|
const byMonth = new Map(plans.map((p) => [p.month, p]));
|
||||||
const years = Array.from(
|
const years = Array.from(
|
||||||
new Set(plans.map((p) => Number(p.month.slice(0, 4))).filter(Boolean))
|
new Set(plans.map((p) => Number(p.month.slice(0, 4))).filter(Boolean))
|
||||||
).sort();
|
).sort((a, b) => b - a);
|
||||||
if (!years.includes(year)) years.push(year);
|
if (!years.includes(year)) years.push(year);
|
||||||
years.sort();
|
years.sort((a, b) => b - a);
|
||||||
const legend = plans[0]?.legend || [
|
const legend = plans[0]?.legend || [
|
||||||
{ label: 'Educativo / serie', color: '#2071cf' },
|
{ label: 'Educativo / serie', color: '#2071cf' },
|
||||||
{ label: 'Familia / bienestar', color: '#E08E2E' },
|
{ label: 'Familia / bienestar', color: '#E08E2E' },
|
||||||
|
|||||||
@@ -179,9 +179,22 @@ export default function RrssMonthView({
|
|||||||
const draggingId = React.useRef<string | null>(null);
|
const draggingId = React.useRef<string | null>(null);
|
||||||
const suppressClick = React.useRef(false);
|
const suppressClick = React.useRef(false);
|
||||||
|
|
||||||
|
// Only re-hydrate from server when the month or piece dates actually change.
|
||||||
|
// Avoids snapping a successful drag back if the parent re-renders with a
|
||||||
|
// stale plan object (common on older month views after soft navigation).
|
||||||
|
const serverSig = useMemo(
|
||||||
|
() =>
|
||||||
|
`${plan.month}:${(plan.pieces || [])
|
||||||
|
.map((p) => `${p.id}@${String(p.date || '').slice(0, 10)}`)
|
||||||
|
.join(',')}`,
|
||||||
|
[plan.month, plan.pieces]
|
||||||
|
);
|
||||||
|
const lastServerSig = React.useRef('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (serverSig === lastServerSig.current) return;
|
||||||
|
lastServerSig.current = serverSig;
|
||||||
setLivePieces(plan.pieces || []);
|
setLivePieces(plan.pieces || []);
|
||||||
}, [plan]);
|
}, [serverSig, plan.pieces]);
|
||||||
|
|
||||||
const pieces = livePieces;
|
const pieces = livePieces;
|
||||||
|
|
||||||
@@ -192,10 +205,11 @@ export default function RrssMonthView({
|
|||||||
|
|
||||||
const filtered = pieces.filter((p) => {
|
const filtered = pieces.filter((p) => {
|
||||||
if (platform === 'all') return true;
|
if (platform === 'all') return true;
|
||||||
|
const pl = String(p.platform || '').toLowerCase();
|
||||||
if (platform === 'instagram') {
|
if (platform === 'instagram') {
|
||||||
return p.platform === 'instagram' || p.platform === 'facebook';
|
return pl === 'instagram' || pl === 'facebook';
|
||||||
}
|
}
|
||||||
return p.platform === platform;
|
return pl === platform;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ids = pieces.map((p) => p.id);
|
const ids = pieces.map((p) => p.id);
|
||||||
@@ -641,16 +655,21 @@ export default function RrssMonthView({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() => openDay(key, dayPieces[0])}
|
onClick={() => openDay(key, dayPieces[0])}
|
||||||
className={`min-h-[112px] rounded-lg border bg-white p-1.5 text-left cursor-pointer overflow-hidden ${
|
className={`min-h-[128px] rounded-lg border bg-white p-1.5 text-left cursor-pointer ${
|
||||||
over
|
over
|
||||||
? 'border-brand-500 ring-2 ring-brand-200 bg-brand-50/40'
|
? 'border-brand-500 ring-2 ring-brand-200 bg-brand-50/40'
|
||||||
: 'border-gray-200 hover:border-brand-400 hover:shadow-sm'
|
: 'border-gray-200 hover:border-brand-400 hover:shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-[13px] font-bold text-gray-900 mb-1 tabular-nums">
|
<p className="text-[13px] font-bold text-gray-900 mb-1 tabular-nums flex items-center justify-between gap-1">
|
||||||
{day}
|
<span>{day}</span>
|
||||||
|
{dayPieces.length > 1 ? (
|
||||||
|
<span className="text-[10px] font-semibold text-gray-500">
|
||||||
|
{dayPieces.length}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1 max-h-[140px] overflow-y-auto">
|
||||||
{dayPieces.map((p) => {
|
{dayPieces.map((p) => {
|
||||||
const tone = chipTone(p);
|
const tone = chipTone(p);
|
||||||
const kind =
|
const kind =
|
||||||
|
|||||||
Reference in New Issue
Block a user