From 06582f51be9f4edaba41c987c848131df69f5062 Mon Sep 17 00:00:00 2001 From: Muhammad Afif Date: Wed, 22 Oct 2025 10:21:27 +0700 Subject: [PATCH] add: filter schedule data harian/mingguan/bulanan --- config/index.js | 88 +++++++++++++++++++++++++++++++++++++---------- db/schedule.db.js | 68 +++++++++++++++++++++++++++++------- 2 files changed, 125 insertions(+), 31 deletions(-) diff --git a/config/index.js b/config/index.js index fe0b713..7c699b1 100644 --- a/config/index.js +++ b/config/index.js @@ -31,11 +31,11 @@ const poolPromise = new sql.ConnectionPool(config) async function checkConnection() { try { const pool = await poolPromise; - await pool.request().query('SELECT 1 AS isConnected'); - console.log('🔍 SQL Server terkoneksi dengan baik'); + await pool.request().query("SELECT 1 AS isConnected"); + console.log("🔍 SQL Server terkoneksi dengan baik"); return true; } catch (error) { - console.error('⚠️ Gagal cek koneksi SQL Server:', error); + console.error("⚠️ Gagal cek koneksi SQL Server:", error); return false; } } @@ -58,13 +58,16 @@ async function query(text, params = []) { return request.query(sqlText); } +/** + * Validasi tanggal + */ function isValidDate(dateStr) { const d = new Date(dateStr); - return !isNaN(d.getTime()); // true kalau valid + return !isNaN(d.getTime()); } /** - * Build filter query + * Build filter query (AND) */ function buildFilterQuery(filterQuery = [], fixedParams = []) { let whereConditions = []; @@ -76,7 +79,9 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { switch (f.type) { case "string": queryParams.push(`%${f.param}%`); - whereConditions.push(`${f.column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS`); + whereConditions.push( + `${f.column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS` + ); break; case "number": @@ -89,10 +94,9 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { whereConditions.push(`${f.column} = $${queryParams.length}`); break; - case 'between': + case "between": if (Array.isArray(f.param) && f.param.length === 2) { - const from = f.param[0]; - const to = f.param[1]; + const [from, to] = f.param; if (isValidDate(from) && isValidDate(to)) { queryParams.push(from); queryParams.push(to); @@ -112,7 +116,7 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { * Build OR ILIKE (SQL Server pakai LIKE + COLLATE) */ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { - if (!criteria) return { whereClause: "", whereParam: fixedParams }; + if (!criteria) return { whereOrConditions: "", whereParamOr: fixedParams }; let orStringConditions = []; let queryParams = [...fixedParams]; @@ -120,7 +124,9 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { columnParam.forEach((column) => { if (!column) return; queryParams.push(`%${criteria}%`); - orStringConditions.push(`${column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS`); + orStringConditions.push( + `${column} LIKE $${queryParams.length} COLLATE SQL_Latin1_General_CP1_CI_AS` + ); }); const whereClause = orStringConditions.length @@ -130,12 +136,60 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { return { whereOrConditions: whereClause, whereParamOr: queryParams }; } +/** + * Build Date Filter (harian / mingguan / bulanan) + */ +function buildDateFilter(column, type, dateValue, fixedParams = []) { + let whereCondition = ""; + let queryParams = [...fixedParams]; + + if (!dateValue && type !== "monthly") { + return { whereDateCondition: "", whereDateParams: queryParams }; + } + + switch (type) { + case "daily": { + queryParams.push(dateValue); + whereCondition = `CAST(${column} AS DATE) = $${queryParams.length}`; + break; + } + + case "weekly": { + const startDate = new Date(dateValue); + if (!isNaN(startDate.getTime())) { + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 6); + + queryParams.push(startDate.toISOString().split("T")[0]); + queryParams.push(endDate.toISOString().split("T")[0]); + + whereCondition = `CAST(${column} AS DATE) BETWEEN $${queryParams.length - 1} AND $${queryParams.length}`; + } + break; + } + + case "monthly": { + const [year, month] = dateValue.split("-"); + if (year && month) { + queryParams.push(parseInt(year), parseInt(month)); + whereCondition = `YEAR(${column}) = $${queryParams.length - 1} AND MONTH(${column}) = $${queryParams.length}`; + } + break; + } + + default: + whereCondition = ""; + } + + return { whereDateCondition: whereCondition, whereDateParams: queryParams }; +} + + /** * Build dynamic UPDATE */ function buildDynamicUpdate(table, data, where) { - - data.updated_by = data.userId + data.updated_by = data.userId; delete data.userId; const setParts = []; @@ -153,7 +207,6 @@ function buildDynamicUpdate(table, data, where) { throw new Error("Tidak ada kolom untuk diupdate"); } - // updated_at otomatis pakai CURRENT_TIMESTAMP setParts.push(`updated_at = CURRENT_TIMESTAMP`); const whereParts = []; @@ -175,9 +228,8 @@ function buildDynamicUpdate(table, data, where) { * Build dynamic INSERT */ function buildDynamicInsert(table, data) { - - data.created_by = data.userId - data.updated_by = data.userId + data.created_by = data.userId; + data.updated_by = data.userId; delete data.userId; const columns = []; @@ -197,7 +249,6 @@ function buildDynamicInsert(table, data) { throw new Error("Tidak ada kolom untuk diinsert"); } - // created_at & updated_at otomatis columns.push("created_at", "updated_at"); placeholders.push("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"); @@ -238,6 +289,7 @@ module.exports = { checkConnection, query, buildFilterQuery, + buildDateFilter, buildStringOrIlike, buildDynamicInsert, buildDynamicUpdate, diff --git a/db/schedule.db.js b/db/schedule.db.js index 807a085..a54a0f1 100644 --- a/db/schedule.db.js +++ b/db/schedule.db.js @@ -1,7 +1,11 @@ const pool = require("../config"); const { formattedDate } = require("../utils/date"); -// Get all schedules +const normalizeClause = (clause) => { + if (!clause) return ""; + return clause.replace(/^\s*(?:AND|WHERE)\s*/i, "").trim(); +}; + const getAllScheduleDb = async (searchParams = {}) => { let queryParams = []; @@ -18,11 +22,51 @@ const getAllScheduleDb = async (searchParams = {}) => { if (whereParamOr) queryParams = whereParamOr; const { whereConditions, whereParamAnd } = pool.buildFilterQuery( - [{ column: "a.schedule_date", param: searchParams.name, type: "date" }], + [ + { + column: "a.schedule_date", + param: searchParams.name, + type: "date", + }, + ], queryParams ); if (whereParamAnd) queryParams = whereParamAnd; + const { whereDateCondition, whereDateParams } = pool.buildDateFilter( + "a.schedule_date", + searchParams.dateType, + searchParams.dateValue, + queryParams + ); + if (whereDateParams) queryParams = whereDateParams; + + const whereParts = []; + + whereParts.push("a.deleted_at IS NULL"); + + if (Array.isArray(whereConditions) && whereConditions.length > 0) { + const joined = whereConditions.join(" AND "); + const norm = normalizeClause(joined); + if (norm) whereParts.push(norm); + } else if (typeof whereConditions === "string" && whereConditions.trim()) { + const norm = normalizeClause(whereConditions); + if (norm) whereParts.push(norm); + } + + if (whereOrConditions && String(whereOrConditions).trim()) { + const norm = normalizeClause(whereOrConditions); + if (norm) whereParts.push(norm); + } + + if (whereDateCondition && String(whereDateCondition).trim()) { + const norm = normalizeClause(whereDateCondition); + if (norm) whereParts.push(norm); + } + + const whereClause = + whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : ""; + const queryText = ` SELECT COUNT(*) OVER() AS total_data, @@ -32,14 +76,13 @@ const getAllScheduleDb = async (searchParams = {}) => { b.end_time FROM schedule a LEFT JOIN m_shift b ON a.shift_id = b.shift_id - WHERE a.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} - ${whereOrConditions ? ` ${whereOrConditions}` : ""} + ${whereClause} ORDER BY a.schedule_id ASC - ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ''} + ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ""} `; const result = await pool.query(queryText, queryParams); + const total = result?.recordset?.length > 0 ? parseInt(result.recordset[0].total_data, 10) @@ -48,6 +91,7 @@ const getAllScheduleDb = async (searchParams = {}) => { return { data: result.recordset, total }; }; +// Get by ID const getScheduleByIdDb = async (id) => { const queryText = ` SELECT @@ -63,8 +107,9 @@ const getScheduleByIdDb = async (id) => { return result.recordset?.[0] || null; }; +// Insert (bisa multi hari) const insertScheduleDb = async (store) => { - const nextDays = Number(store.next_day ?? 0); // default 0 kalau tidak diisi + const nextDays = Number(store.next_day ?? 0); const insertedRecords = []; for (let i = 0; i <= nextDays; i++) { @@ -72,11 +117,7 @@ const insertScheduleDb = async (store) => { nextDate.setDate(nextDate.getDate() + i); const formatted = formattedDate(nextDate); - - const newStore = { - ...store, - schedule_date: formatted, - }; + const newStore = { ...store, schedule_date: formatted }; delete newStore.next_day; const { query: queryText, values } = pool.buildDynamicInsert("schedule", newStore); @@ -92,6 +133,7 @@ const insertScheduleDb = async (store) => { return insertedRecords; }; +// Update const updateScheduleDb = async (id, data) => { const store = { ...data }; const whereData = { schedule_id: id }; @@ -106,7 +148,7 @@ const updateScheduleDb = async (id, data) => { return getScheduleByIdDb(id); }; -// Soft delete schedule +// Soft delete const deleteScheduleDb = async (id, deletedBy) => { const queryText = ` UPDATE schedule