معادن · الصيانة الميكانيكية PAP · الحضور والقوى العاملة

داشبورد الحضور: صفحة واحدة

صفحة واحدة (6 مؤشرات و5 رسوم) مصممة على أساس أفضل ممارسات داشبوردات الحضور (Microsoft، Zebra BI، IBCS، AIHR) ومبنية بالكامل داخل Power BI بعناصره الأصلية (بدون أي صورة خلفية)، بهوية معادن، تقرأ ثلاثة ملفات: ملف الحضور بالماكرو على OneDrive، وماستر الموظفين على SharePoint، وقائمة المقاول (السويدي). الملف الجاهز على جهازك، وهذا الدليل يعلمك تبنينه بنفسك عنصر عنصر، وكيف تربطين الملفات عشان يتحدث الواحد ويظهر في الثاني مباشرة.

قبل ما تبدين

الصورة الكاملة

الملف الجاهز: 02_Attendance\2_Dashboard\PAP_Workforce_Dashboard.pbip (افتحيه في Power BI Desktop، اضغطي Refresh). المولّد 5_Scripts\att_workforce_pbip.py يعيد بناءه بأمر واحد لو تغير شي.

ملف الحضور (OneDrive)Daily Attendance.xlsm، جدول AttendanceTracker: التاريخ، الرقم، الحالة. يكتبه المسؤول يوميًا.
ماستر الموظفين (SharePoint)PAP_Mech_Employee_Master.xlsx، جدول tblEmployees: سطر لكل موظف، رقم معادن، الشفت الافتراضي.
قائمة المقاولMasterSheet من السويدي، جدول Table7: الحالة، تاريخ التعبئة، الإجازة، انتهاء الهوية.
Power BIيقرأ الثلاثة، يربطهم بالرقم، يحسب، وينشر على SharePoint ويتحدث بجدولة.

وش تجاوب الصفحة (من فوق لتحت)

الجزءالسؤالالعنصر
العنوانجملة تتغير مع الفلتر: الفترة، النسبة، والهدفCard بمقياس [Title Text]: "Attendance 01 Sep to 05 Sep 2026: 93.4% of scheduled days on site, target 95%"
6 مؤشراتنسبة الحضور (مع الفرق عن الفترة السابقة)، الغياب غير المخطط، الإجازة المخططة، الموجودون، اكتمال التسجيل، الهيدكاونتAttendance rate · Absenteeism, unplanned · Planned leave · On site · Records coverage · Active headcount
الصف الأولالاتجاه يوم بيوم مقابل هدف 95%؟ وأي منطقة أعلى وأي منطقة أدنى؟Line (نسبة الحضور + خط الهدف المتقطع) · Bar بالمناطق مرتب تنازليًا ولونه يميل للنحاسي تحت الهدف
الصف الثانيأي منطقة في أي يوم كانت ضعيفة؟ أي يوم من الأسبوع أضعف؟ ومين يحتاج سؤال؟Heatmap (منطقة × يوم) · Columns بأيام الأسبوع · جدول Needs attention (غياب × 3 + مرض × 2 + أيام بدون تسجيل)

المعادلات (تعريفات AIHR و GoodShape)

المؤشرالمعادلةليش
Scheduled daysRecords − Day off − Exitيوم الراحة الأسبوعي والخروج ليست أيام عمل، فلا تُحسب في المقام
Attendance rateOn site ÷ Scheduled daysالحضور نسبة من أيام العمل المجدولة، لا من كل السجلات
Absenteeism (unplanned)(Absent + Sick) ÷ Scheduled daysAIHR: احسبي الغياب غير المخطط فقط؛ الإجازة الموافق عليها شي ثاني. المرجع العالمي 1.5%
Planned leave(Vacation + Permission) ÷ Scheduled daysمخطط ومعروف مسبقًا، ما يُعاقب عليه القسم
Records coverageRecords ÷ (Active headcount × days)ملفك لقطة يومية فيها فجوات؛ بدون هذا الرقم الفجوة تنقرأ غياب
Δ vs previous periodRate − Rate(نفس عدد الأيام قبلها)Zebra BI: الرقم بلا مقارنة ما يعني شي. يظهر بسهم ▲▼ تحت البطاقة
Attention scoreAbsent × 3 + Sick × 2 + Missed daysترتيب بسيط لمين تسألين عنه أول (بديل مبسط لعامل Bradford)

قواعد التصميم اللي مشينا عليها (ومن وين)

  • شاشة واحدة بدون تمرير، والأهم فوق يسار (Stephen Few، Microsoft): البطاقات أولًا، الاتجاه ثانيًا، التفاصيل تحت.
  • ثمانية عناصر كحد أقصى وجدول واحد (MAQ Software، Zebra BI): عندنا 5 رسوم وجدول واحد.
  • كل بطاقة تحمل مقارنة (Zebra BI): هدف أو فترة سابقة، لا رقم عاري.
  • الأعمدة أفضل من الدوائر (Microsoft Learn: Pie وDonut وGauge "ليست مثالية")؛ لذلك حذفنا القيج والدونات.
  • ترتيب الأعمدة تنازليًا حين تكون الرسالة ترتيبًا (Microsoft).
  • الزمن أفقي، المقارنة بين الفئات عمودية (IBCS).
  • العنوان جملة: الموضوع + المقياس + الفترة + الهدف (IBCS، Zebra BI).
  • 3 إلى 5 ألوان ولون واحد للتنبيه (Lukas Reese، Zebra BI): Phosphate أساسي، Gold ثانوي، Copper للهدف وما تحته، Crimson للغياب فقط.
  • شبكة 8px: هامش 32، فراغ 16، ارتفاعات مضاعفات 8 (Zebra BI، Lukas Reese).
  • خط الهدف بدل سلسلة زيادة (Microsoft Analytics pane): عندنا مقياس ثابت Target 95% كخط متقطع.
  • Heatmap = Matrix + تلوين شرطي (Coupler، Microsoft): أغمق = أفضل.
ليش صفحة واحدةالحضور يُقرأ في دقيقة: رقم مع مقارنة، اتجاه مع هدف، ترتيب المناطق، ومين ناقص. كل شي ثاني (الهويات، الإجازات، قائمة المقاول) موجود في النموذج كمقاييس جاهزة، وتقدرين تضيفين له صفحة لاحقًا لو احتجتيه، لكن الصفحة اليومية تبقى وحدة.
ليش بدون صور خلفيةكل مستطيل ونص وبطاقة هنا عنصر Power BI أصلي. تقدرين تعدلين أي شي بالفورمات، وتنسخين الصفحة بـ Duplicate، والتقرير خفيف وينطبع PDF صح. الشكل يجي من ثلاث حاجات بس: مستطيلات بيضاء على أرضية Bone، خط ذهبي تحت الترويسة، وبطاقات أرقام كبيرة.

الخطوة 2

Power Query: الجداول الأربعة

لكل جدول: Get Data ← Blank Query ← Advanced Editor ← الصقي الكود ← سمّي الاستعلام بنفس الاسم. غيري سطر Source فقط.

Employees · ماستر الموظفين31 عمود
  • LinkID: مفتاح الربط
  • TenureYears: من تاريخ التعبئة
  • IDDaysLeft / IDStatus / IDSort: انتهاء الهوية (سالب = منتهية، ≤60، ≤90، Valid، No date)
  • HasMaadenID، AreaClean، CategoryClean، NationalityClean: الفاضي يصير نصًا واضحًا بدل (Blank)
  • وبعدها 5 أعمدة DAX محسوبة (في القسم 3)
let
    Source = Excel.Workbook(File.Contents("C:\\Users\\rawab\\OneDrive\\Desktop\\معادن - روابي\\02_Attendance\\3_Output\\PAP_Mech_Employee_Master.xlsx"), null, true),
    T = Source{[Item="tblEmployees", Kind="Table"]}[Data],
    Kept = Table.SelectColumns(T, {"MaadenID", "SIS_ID", "EmployeeName", "Craft", "Area", "Category", "ContractType", "EmploymentStatus", "Nationality", "Type of ID", "ID Expire Date", "JoiningDate", "ExitDate", "WorkingDays", "WorkingHours", "DefaultShift", "Note"}),
    NoBlank = Table.SelectRows(Kept, each [EmployeeName] <> null and [EmployeeName] <> ""),
    Typed = Table.TransformColumnTypes(NoBlank, {{"MaadenID", type text}, {"SIS_ID", type text}, {"EmployeeName", type text}, {"Craft", type text}, {"Area", type text}, {"Category", type text}, {"ContractType", type text}, {"EmploymentStatus", type text}, {"Nationality", type text}, {"Type of ID", type text}, {"ID Expire Date", type date}, {"JoiningDate", type date}, {"ExitDate", type date}, {"WorkingDays", Int64.Type}, {"WorkingHours", Int64.Type}, {"DefaultShift", type text}, {"Note", type text}}),
    Today = Date.From(DateTime.LocalNow()),
    // مفتاح الربط مع ملف الحضور: رقم السويدي للمقاول، ورقم معادن للمباشر
    A1 = Table.AddColumn(Typed, "LinkID", each if [SIS_ID] <> null and Text.Trim([SIS_ID]) <> "" then Text.Trim([SIS_ID]) else Text.Trim([MaadenID]), type text),
    A2 = Table.AddColumn(A1, "TenureYears", each if [JoiningDate] = null then null else Number.Round(Duration.Days(Today - [JoiningDate]) / 365.25, 1), type number),
    A3 = Table.AddColumn(A2, "IDDaysLeft", each if [ID Expire Date] = null then null else Duration.Days([ID Expire Date] - Today), Int64.Type),
    A4 = Table.AddColumn(A3, "IDStatus", each if [ID Expire Date] = null then "No date" else if [IDDaysLeft] < 0 then "Expired" else if [IDDaysLeft] <= 60 then "Due in 60 days" else if [IDDaysLeft] <= 90 then "Due in 90 days" else "Valid", type text),
    A5 = Table.AddColumn(A4, "IDSort", each if [IDStatus] = "Expired" then 1 else if [IDStatus] = "Due in 60 days" then 2 else if [IDStatus] = "Due in 90 days" then 3 else if [IDStatus] = "Valid" then 4 else 5, Int64.Type),
    A6 = Table.AddColumn(A5, "HasMaadenID", each if [MaadenID] = null or Text.Trim([MaadenID]) = "" then "Missing" else "Yes", type text),
    A7 = Table.AddColumn(A6, "AreaClean", each if [Area] = null or Text.Trim([Area]) = "" then "(No area)" else [Area], type text),
    A8 = Table.AddColumn(A7, "CategoryClean", each if [Category] = null or Text.Trim([Category]) = "" then "(No category)" else [Category], type text),
    A9 = Table.AddColumn(A8, "NationalityClean", each if [Nationality] = null or Text.Trim([Nationality]) = "" then "(Not on contractor list)" else [Nationality], type text)
in
    A9
Attendance · سجل الحضور11 عمود
  • Merge مع Employees على LinkID (نفس فكرة XLOOKUP) يجيب DefaultShift
  • Shift: Day/Night كما كُتب، أو On Duty ← الشفت الافتراضي
  • Presence: On site / Absent / Sick leave / Vacation / Day off / Permission / Exit
  • OnSite: 1 أو 0 للجمع
  • InMaster: هل الرقم موجود في الماستر
let
    Source = Excel.Workbook(File.Contents("C:\\Users\\rawab\\OneDrive\\Desktop\\معادن - روابي\\02_Attendance\\0_Source\\Daily_Attendance_Copy_2026-09-14.xlsm"), null, true),
    T = Source{[Item="AttendanceTracker", Kind="Table"]}[Data],
    Kept = Table.SelectColumns(T, {"Date", "Day", "EmployeeID", "EmployeeName", "Status"}),
    NoBlank = Table.SelectRows(Kept, each [EmployeeID] <> null and Text.From([EmployeeID]) <> "" and [Date] <> null),
    Typed = Table.TransformColumnTypes(NoBlank, {{"Date", type date}, {"Day", type text}, {"EmployeeID", type text}, {"EmployeeName", type text}, {"Status", type text}}),
    Trim = Table.TransformColumns(Typed, {{"EmployeeID", Text.Trim, type text}, {"Status", Text.Trim, type text}}),
    // الشفت الافتراضي يجي من ملف الموظفين (Merge = XLOOKUP)
    J = Table.NestedJoin(Trim, {"EmployeeID"}, Employees, {"LinkID"}, "E", JoinKind.LeftOuter),
    X = Table.ExpandTableColumn(J, "E", {"DefaultShift", "LinkID"}, {"DefaultShift", "MatchedID"}),
    B1 = Table.AddColumn(X, "Shift", each if [Status] = "Day" or [Status] = "Night" then [Status] else if [Status] = "On Duty" then (if [DefaultShift] = null then "Day" else [DefaultShift]) else null, type text),
    B2 = Table.AddColumn(B1, "Presence", each if [Shift] <> null then "On site" else if [Status] = "Absent" then "Absent" else if [Status] = "Sick Leave" then "Sick leave" else if [Status] = "Vacation" then "Vacation" else if [Status] = "Off" then "Day off" else if [Status] = "Permission" then "Permission" else if [Status] = "Exit" then "Exit" else "Other", type text),
    B3 = Table.AddColumn(B2, "PresenceSort", each if [Presence] = "On site" then 1 else if [Presence] = "Absent" then 2 else if [Presence] = "Sick leave" then 3 else if [Presence] = "Permission" then 4 else if [Presence] = "Vacation" then 5 else if [Presence] = "Day off" then 6 else if [Presence] = "Exit" then 7 else 8, Int64.Type),
    B4 = Table.AddColumn(B3, "OnSite", each if [Shift] <> null then 1 else 0, Int64.Type),
    B5 = Table.AddColumn(B4, "InMaster", each if [MatchedID] = null then "Not in master" else "Yes", type text),
    Out = Table.RemoveColumns(B5, {"MatchedID"})
in
    Out
Contractor · قائمة المقاول13 عمود
  • إعادة تسمية الأعمدة لأسماء مفهومة
  • SIS كنص (عشان يطابق)
  • التواريخ النصية مثل 0-Jan-00 تصير فاضية بدل ما تكسر التحديث (try … otherwise null)
  • Status و Nationality بـ Text.Proper
let
    Source = Excel.Workbook(File.Contents("C:\\Users\\rawab\\OneDrive\\Desktop\\معادن - روابي\\02_Attendance\\0_Source\\Suwaidi_MasterSheet_2026-09-14.xlsx"), null, true),
    T = Source{[Item="Table7", Kind="Table"]}[Data],
    Kept = Table.SelectColumns(T, {"SIS FUSION NUMBER", "MPC ID #", "MPC ID EXPIRY DATE", "NAME", "UTILIZATION TRADE", "STATUS", "MOBILIZED DATE", "DMOBD", "REASON FOR DMOB", "VAC /EXIT DATE", "NATIONALITY", "REMARKS"}),
    Renamed = Table.RenameColumns(Kept, {{"SIS FUSION NUMBER", "SIS"}, {"MPC ID #", "Maaden ID (contractor)"}, {"MPC ID EXPIRY DATE", "ID expiry (contractor)"}, {"NAME", "Name"}, {"UTILIZATION TRADE", "Trade"}, {"STATUS", "Status"}, {"MOBILIZED DATE", "Mobilized"}, {"DMOBD", "DMOB date"}, {"REASON FOR DMOB", "DMOB reason"}, {"VAC /EXIT DATE", "Vac/Exit date"}, {"NATIONALITY", "Nationality"}, {"REMARKS", "Remarks"}}),
    NoBlank = Table.SelectRows(Renamed, each [SIS] <> null and Text.From([SIS]) <> ""),
    AsText = Table.TransformColumns(NoBlank, {{"SIS", each Text.Trim(Text.From(_)), type text}, {"Maaden ID (contractor)", each if _ = null then null else Text.Trim(Text.From(_)), type text}}),
    // التاريخ اللي يوصل نص مثل 0-Jan-00 يصير فاضي بدل ما يكسر التحديث
    Dates = Table.TransformColumns(AsText, {{"ID expiry (contractor)", each try Date.From(_) otherwise null, type date}, {"Mobilized", each try Date.From(_) otherwise null, type date}, {"DMOB date", each try Date.From(_) otherwise null, type date}, {"Vac/Exit date", each try Date.From(_) otherwise null, type date}}),
    Proper = Table.TransformColumns(Dates, {{"Status", each Text.Proper(Text.Trim(Text.From(_))), type text}, {"Nationality", each if _ = null then null else Text.Proper(Text.Trim(Text.From(_))), type text}, {"Name", each if _ = null then null else Text.Proper(Text.Trim(Text.From(_))), type text}}),
    Typed = Table.TransformColumnTypes(Proper, {{"Trade", type text}, {"DMOB reason", type text}, {"Remarks", type text}})
in
    Typed
Calendar · التقويم9 عمود
  • من 1 أغسطس 2026 لسنة ونص
  • DayName مرتب بالأحد أولًا، Week، MonthName، IsFriSat
let
    D0 = #date(2026, 8, 1),
    Days = List.Dates(D0, 517, #duration(1, 0, 0, 0)),
    T = Table.FromList(Days, Splitter.SplitByNothing(), {"Date"}),
    T1 = Table.TransformColumnTypes(T, {{"Date", type date}}),
    T2 = Table.AddColumn(T1, "DayName", each Date.ToText([Date], "ddd"), type text),
    T3 = Table.AddColumn(T2, "DaySort", each Date.DayOfWeek([Date], Day.Sunday), Int64.Type),
    T4 = Table.AddColumn(T3, "WeekStart", each Date.StartOfWeek([Date], Day.Sunday), type date),
    T5 = Table.AddColumn(T4, "Week", each "W" & Text.From(Date.WeekOfYear([Date], Day.Sunday)), type text),
    T6 = Table.AddColumn(T5, "WeekSort", each Date.Year([Date]) * 100 + Date.WeekOfYear([Date], Day.Sunday), Int64.Type),
    T7 = Table.AddColumn(T6, "MonthName", each Date.ToText([Date], "MMM yyyy"), type text),
    T8 = Table.AddColumn(T7, "MonthSort", each Date.Year([Date]) * 100 + Date.Month([Date]), Int64.Type),
    T9 = Table.AddColumn(T8, "IsFriSat", each if Date.DayOfWeek([Date], Day.Sunday) >= 5 then "Fri/Sat" else "Workday", type text)
in
    T9

ليش الأعمدة المحسوبة في Power Query مو في DAX؟ لأنها ثابتة لكل صف (تاريخ، نص). DAX للأعمدة اللي تحتاج علاقة (مثل "هل موجود عند المقاول") وللمقاييس اللي تتغير مع الفلتر.

الخطوة 3

النموذج والعلاقات

من (الكثير)إلى (الواحد)الاتجاهليش
Attendance[EmployeeID]Employees[LinkID]Many to one · Singleكل سطر حضور يعرف صاحبه ومنطقته وكرافته
Attendance[Date]Calendar[Date]Many to one · Singleمحور التاريخ والأسبوع واليوم
Contractor[SIS]Employees[LinkID]Many to one · Singleكل سطر حضور يعرف صاحبه ومنطقته وكرافته

أعمدة DAX المحسوبة (Modeling ← New column)

هذي تحتاج العلاقة، فمكانها DAX:

Employees[InContractorList]
IF ( CALCULATE ( COUNTROWS ( Contractor ) ) > 0, "Yes", IF ( Employees[ContractType] = "Direct", "n/a (direct)", "No" ) )
Employees[ContractorStatus]
CALCULATE ( MAX ( Contractor[Status] ) )
Employees[StatusMatch]
VAR c = Employees[ContractorStatus] RETURN IF ( ISBLANK ( c ), "n/a", IF ( c = Employees[EmploymentStatus], "Match", "Mismatch: ours " & Employees[EmploymentStatus] & " / contractor " & c ) )
Employees[VacationFrom]
IF ( Employees[EmploymentStatus] = "Vacation", CALCULATE ( MAX ( Contractor[DMOB date] ) ) )
Employees[SyncIssue]
SWITCH ( TRUE (), Employees[InContractorList] = "No", "Not on contractor list", LEFT ( Employees[StatusMatch], 8 ) = "Mismatch", Employees[StatusMatch], "OK" )
Contractor[InMaster]
IF ( ISBLANK ( RELATED ( Employees[MaadenID] ) ) && ISBLANK ( RELATED ( Employees[EmployeeName] ) ), "New: not in our master", "In master" )

Sort by column وإخفاء

  • IDStatus يترتب بـ IDSort، وPresence بـ PresenceSort، وDayName بـ DaySort، وWeek بـ WeekSort، وMonthName بـ MonthSort.
  • أخفي: LinkID، IDSort، PresenceSort، OnSite، DaySort، WeekSort، MonthSort.
  • علّمي Calendar كـ Date table (كليك يمين ← Mark as date table).

الخطوة 4

المقاييس: 61 مقياسًا في جدول _Measures

Enter data ← اسمه _Measures ← Load. ثم لكل مقياس New measure والصقي. المجلدات (Display folder) تخلي القائمة مرتبة.

01 Attendance · الحضور: عدّ الصفوف حسب Presence20 مقياس
RecordsFormat: #,0
COUNTROWS ( Attendance ) + 0
On SiteFormat: #,0
SUM ( Attendance[OnSite] ) + 0
Day ShiftFormat: #,0
CALCULATE ( [On Site], Attendance[Shift] = "Day" )
Night ShiftFormat: #,0
CALCULATE ( [On Site], Attendance[Shift] = "Night" )
Att AbsentFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Absent" )
Att Sick leaveFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Sick leave" )
Att VacationFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Vacation" )
Att Day offFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Day off" )
Att PermissionFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Permission" )
Att ExitFormat: #,0
CALCULATE ( [Records], Attendance[Presence] = "Exit" )
People SeenFormat: #,0
DISTINCTCOUNT ( Attendance[EmployeeID] ) + 0
Days In ViewFormat: #,0
CALCULATE ( DISTINCTCOUNT ( Attendance[Date] ) ) + 0
Active HeadcountFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] = "Active" ) + 0
Expected RecordsFormat: #,0
[Active Headcount] * [Days In View]
Not RecordedFormat: #,0
MAX ( 0, [Expected Records] - [Records] )
Man DaysFormat: #,0
[On Site]
Scheduled DaysFormat: #,0
MAX ( 0, [Records] - [Att Day off] - [Att Exit] )
Days In View AllFormat: #,0
CALCULATE ( DISTINCTCOUNT ( Attendance[Date] ), REMOVEFILTERS ( Employees ), REMOVEFILTERS ( Attendance[EmployeeID] ) ) + 0
Missed DaysFormat: #,0
IF ( SELECTEDVALUE ( Employees[EmploymentStatus] ) = "Active", MAX ( 0, [Days In View All] - [Records] ), 0 )
Attention ScoreFormat: #,0
[Att Absent] * 3 + [Att Sick leave] * 2 + [Missed Days]
02 Ratios · النسب: القسمة الآمنة بـ DIVIDE16 مقياس
Coverage %Format: 0.0%
DIVIDE ( [Records], [Expected Records] )
Attendance %Format: 0.0%
DIVIDE ( [On Site], [Records] )
Absence %Format: 0.0%
DIVIDE ( [Att Absent] + [Att Sick leave], [Records] )
Leave %Format: 0.0%
DIVIDE ( [Att Vacation] + [Att Day off], [Records] )
Night Share %Format: 0.0%
DIVIDE ( [Night Shift], [On Site] )
Avg On Site per DayFormat: #,0.0
DIVIDE ( [On Site], [Days In View] )
Last RecordedFormat: dd MMM yyyy
MAX ( Attendance[Date] )
Target 100Format: 0%
1
Max 100Format: 0%
1
Attendance Rate %Format: 0.0%
DIVIDE ( [On Site], [Scheduled Days] )
Absenteeism %Format: 0.0%
DIVIDE ( [Att Absent] + [Att Sick leave], [Scheduled Days] )
Planned Leave %Format: 0.0%
DIVIDE ( [Att Vacation] + [Att Permission], [Scheduled Days] )
Target 95%Format: 0%
0.95
Benchmark 1.5%Format: 0.0%
0.015
Attendance Rate PrevFormat: 0.0%
VAR n = [Days In View] VAR d1 = MIN ( Attendance[Date] ) RETURN IF ( n > 0, CALCULATE ( [Attendance Rate %], DATESBETWEEN ( Calendar[Date], d1 - n, d1 - 1 ) ) )
Absenteeism PrevFormat: 0.0%
VAR n = [Days In View] VAR d1 = MIN ( Attendance[Date] ) RETURN IF ( n > 0, CALCULATE ( [Absenteeism %], DATESBETWEEN ( Calendar[Date], d1 - n, d1 - 1 ) ) )
03 Workforce · القوى العاملة: من الماستر10 مقياس
HeadcountFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] IN { "Active", "Vacation" } ) + 0
On VacationFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] = "Vacation" ) + 0
ExitedFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] IN { "Exit", "Resigned", "Demobilized" } ) + 0
Direct StaffFormat: #,0
CALCULATE ( [Headcount], Employees[ContractType] = "Direct" )
Subcontract StaffFormat: #,0
CALCULATE ( [Headcount], Employees[ContractType] = "Subcontract" )
Permanent %Format: 0%
DIVIDE ( CALCULATE ( [Headcount], Employees[Type of ID] IN { "Permanent", "Permanent (Old ID)", "Maaden Staff" } ), [Headcount] )
TAM CountFormat: #,0
CALCULATE ( [Headcount], Employees[Type of ID] = "TAM" )
Avg TenureFormat: 0.0
AVERAGE ( Employees[TenureYears] )
Night DefaultFormat: #,0
CALCULATE ( [Headcount], Employees[DefaultShift] = "Night" )
No Maaden IDFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[HasMaadenID] = "Missing" ) + 0
04 IDs & leave · الهويات والإجازات5 مقياس
IDs ExpiredFormat: #,0
CALCULATE ( [Headcount], Employees[IDStatus] = "Expired" )
IDs Due 60Format: #,0
CALCULATE ( [Headcount], Employees[IDStatus] = "Due in 60 days" )
IDs Due 90Format: #,0
CALCULATE ( [Headcount], Employees[IDStatus] = "Due in 90 days" )
IDs No DateFormat: #,0
CALCULATE ( [Headcount], Employees[IDStatus] = "No date" )
Employees (count)Format: #,0
COUNTROWS ( Employees ) + 0
05 Contractor & quality · المقاول وجودة البيانات6 مقياس
Contractor RowsFormat: #,0
COUNTROWS ( Contractor ) + 0
New In Contractor ListFormat: #,0
CALCULATE ( COUNTROWS ( Contractor ), Contractor[InMaster] = "New: not in our master" ) + 0
Master Not In ContractorFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[InContractorList] = "No" ) + 0
Status MismatchFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), LEFT ( Employees[StatusMatch], 8 ) = "Mismatch", Employees[EmploymentStatus] IN { "Active", "Vacation" } ) + 0
Att IDs Not In MasterFormat: #,0
CALCULATE ( DISTINCTCOUNT ( Attendance[EmployeeID] ), Attendance[InMaster] = "Not in master" ) + 0
Blank Area or CategoryFormat: #,0
CALCULATE ( COUNTROWS ( Employees ), Employees[AreaClean] = "(No area)" || Employees[CategoryClean] = "(No category)" ) + 0
06 Labels · النصوص الصغيرة تحت الأرقام25 مقياس
Headline
"Last day recorded " & FORMAT ( [Last Recorded], "dd MMM yyyy" ) & " · " & FORMAT ( [Active Headcount], "#,0" ) & " active employees"
Sub On Site
"Day " & FORMAT ( [Day Shift], "#,0" ) & " · night " & FORMAT ( [Night Shift], "#,0" )
Sub Coverage
"Recorded " & FORMAT ( [Records], "#,0" ) & " of " & FORMAT ( [Expected Records], "#,0" ) & " expected"
Sub Absent
"Absent " & FORMAT ( [Att Absent], "#,0" ) & " · sick " & FORMAT ( [Att Sick leave], "#,0" )
Sub Leave
"Vacation " & FORMAT ( [Att Vacation], "#,0" ) & " · off " & FORMAT ( [Att Day off], "#,0" )
Sub Days
"Across " & FORMAT ( [Days In View], "#,0" ) & " day(s) in the filter"
Sub People
"Distinct people seen " & FORMAT ( [People Seen], "#,0" )
Sub Headcount
"Direct " & FORMAT ( [Direct Staff], "#,0" ) & " · subcontract " & FORMAT ( [Subcontract Staff], "#,0" )
Sub Vacation
"Exited or moved " & FORMAT ( [Exited], "#,0" )
Sub Permanent
"TAM " & FORMAT ( [TAM Count], "#,0" ) & " · night default " & FORMAT ( [Night Default], "#,0" )
Sub Tenure
"Years since mobilisation, average"
Sub IDs
"No expiry date on file: " & FORMAT ( [IDs No Date], "#,0" )
Sub Missing ID
"Cannot be linked to attendance"
Sub New
"On Al Suwaidi list, missing from our master"
Sub NotInContractor
"Subcontract in our master, not on their list"
Sub Mismatch
"Our status vs contractor status"
Sub AttIDs
"Numbers typed in attendance with no employee"
Period Label
VAR d0 = MIN ( Attendance[Date] ) VAR d1 = MAX ( Attendance[Date] ) RETURN IF ( ISBLANK ( d0 ), "no records", IF ( d0 = d1, FORMAT ( d0, "dd MMM yyyy" ), FORMAT ( d0, "dd MMM" ) & " to " & FORMAT ( d1, "dd MMM yyyy" ) ) )
Title Text
"Attendance " & [Period Label] & ": " & FORMAT ( [Attendance Rate %], "0.0%" ) & " of scheduled days on site, target 95%"
Sub Attendance
VAR p = [Attendance Rate Prev] VAR d = [Attendance Rate %] - p RETURN IF ( ISBLANK ( p ), "No previous period to compare", IF ( d >= 0, UNICHAR ( 9650 ), UNICHAR ( 9660 ) ) & " " & FORMAT ( ABS ( d ) * 100, "0.0" ) & " pp vs previous " & FORMAT ( [Days In View], "#,0" ) & " days" )
Sub Absenteeism
VAR p = [Absenteeism Prev] VAR d = [Absenteeism %] - p RETURN "Absent " & FORMAT ( [Att Absent], "#,0" ) & " · sick " & FORMAT ( [Att Sick leave], "#,0" ) & IF ( ISBLANK ( p ), "", " · " & IF ( d > 0, UNICHAR ( 9650 ), UNICHAR ( 9660 ) ) & " " & FORMAT ( ABS ( d ) * 100, "0.0" ) & " pp" )
Sub Planned
"Vacation " & FORMAT ( [Att Vacation], "#,0" ) & " · permission " & FORMAT ( [Att Permission], "#,0" )
Sub Coverage v3
"Recorded " & FORMAT ( [Records], "#,0" ) & " of " & FORMAT ( [Expected Records], "#,0" ) & " · missing " & FORMAT ( [Not Recorded], "#,0" )
Sub Headline
"Last day recorded " & FORMAT ( [Last Recorded], "dd MMM yyyy" ) & " · " & FORMAT ( [Active Headcount], "#,0" ) & " active · " & FORMAT ( [Scheduled Days], "#,0" ) & " scheduled days"
Heat Colour
VAR r = [Attendance Rate %] RETURN SWITCH ( TRUE (), ISBLANK ( r ), "#FFFFFF", r >= 0.95, "#B8A567", r >= 0.85, "#D9CFA3", "#F1EACB" )
ثلاث أفكار تفسر أغلب المقاييسCoverage = السجلات ÷ (الموظفين النشطين × عدد الأيام في الفلتر). لو قسمتي على الموظفين بس تطلع 300% في أسبوع.
Not recorded = المتوقع − المسجل، ولا ينزل تحت الصفر.
+ 0 في آخر مقاييس العد عشان البطاقة تكتب 0 بدل فراغ. وفي الرسوم نضيف فلتر EmploymentStatus عشان الصفر ما يصنع فئة (Blank).

الخطوة 5

نظام التصميم: كل شي من ثلاث وصفات

الصفحة 1280 × 720. الهامش 48 من اليمين واليسار (المحتوى من x=48 إلى 1232). الفراغ بين العناصر 12. الخط Segoe UI في كل شي.

اللونالكودوين
Bone#F7F3E9أرضية الصفحة (Format page ← Canvas background)
أبيض#FFFFFFكل مستطيل وبطاقة
Phosphate#343631الأرقام الكبيرة، العناوين، الأعمدة الرئيسية
Gold#B8A567الخط تحت الترويسة، السلسلة الثانية (Night)، التبويب النشط
Iron / Aluminum#5F5E5B / #BDBAB5النصوص الصغيرة، الملاحظات
Copper / Crimson#C76210 / #8A340Fتحذير / سيء (الغياب، المنتهي)
Moonstone#E0DEDBالحدود والخطوط الفاصلة
الوصفة 1: الترويسة 1) Insert ← Shapes ← Rectangle: x0 y0 w1280 h66، تعبئة أبيض، بدون حد.
2) مستطيل ثاني: x0 y66 w1280 h3، تعبئة Gold. هذا الخط الذهبي.
3) Insert ← Image: شعار معادن x48 y15 w168 h32.
4) Text box: "PHOSPHORIC ACID PLANT · MECHANICAL MAINTENANCE · WORKFORCE" حجم 7 عريض Gold، x238 y3.
5) Card بالمقياس [Title Text] كعنوان: Callout حجم 13 Segoe UI Semibold لون Phosphate، Category label off، x218 y20 w760 h34. يتغير مع الفلتر.
6) Card بالمقياس [Sub Headline] حجم 7.5 رمادي، x908 y30 w340 (آخر يوم مسجل، النشطون، أيام العمل المجدولة).
الترويسة ارتفاعها 56 والخط الذهبي عند y=56. لا أزرار تنقل: صفحة واحدة.
الوصفة 2: شريط الفلاتر (y=64، ارتفاع 34) مستطيل أبيض x32 y64 w1216 h34، وعليه 5 slicers بأسلوب Dropdown مع label نصي قبل كل واحد: DATES (Calendar[Date]، Between) · AREA (Employees[AreaClean]) · CRAFT (Employees[Craft]) · CATEGORY (Employees[CategoryClean]) · CONTRACT (Employees[ContractType]) · زر CLEAR (Action = Clear all slicers).
Format للـ slicer: Header off، Values حجم 8، خلفية أبيض.
الوصفة 3: بطاقة مؤشر (KPI tile) كل واحدة 189 × 88 من y=110 (ست بطاقات بفراغ 16 تملأ العرض 1216):
1) مستطيل أبيض 189×88.
2) مستطيل رفيع فوقه 189×3 بلون البطاقة (Phosphate / Crimson / Copper / Gold / Aluminum / Earth).
3) Text box بالعنوان بأحرف كبيرة، حجم 7 عريض Iron، y+6.
4) Card بالمقياس الرئيسي: Callout value حجم 20 Phosphate (أو Crimson للسيء)، Category label off، y+28.
5) Card بمقياس النص الصغير (Sub …): حجم 7.5 Iron، y+60.
بعد أول بطاقة: Group ← Copy ← Paste خمس مرات، وغيري المقياسين واللون بس.
ATTENDANCE RATE93.4%▲ x.x pp vs previous 5 days
ABSENTEEISM, UNPLANNED0.3%Absent 2 · sick 0
PLANNED LEAVE6.3%Vacation 36 · permission 0
ON SITE537Day 501 · night 36
RECORDS COVERAGE70.4%Recorded 591 of 840 · missing 249
ACTIVE HEADCOUNT168Direct 9 · subcontract 172
كل رسم أو جدول Format ← General ← Title on (حجم 11 Phosphate يسار) + Subtitle on (حجم 9 رمادي) · Effects ← Background أبيض، Border on بلون Moonstone نصف قطر 4، Shadow off · Visual header off.
الرسوم: محور X حجم 9 رمادي، عنوان المحاور off، خطوط الشبكة فاتحة #EFEAE0، Data labels حجم 8 حيث مذكور.
الجداول: رأس الأعمدة خلفية Bone نص Iron حجم 8، القيم حجم 8 مع صفوف متبادلة أبيض/#FBF9F4، خطوط أفقية فقط، Totals off.

الصفوف: البطاقات من y=110 بارتفاع 88، الصف الأول من y=214 بارتفاع 224 (Line عرض 800، ثم Bar عرض 400)، الصف الثاني من y=454 بارتفاع 222 (Heatmap 456، Columns 304، الجدول 424)، والتذييل عند y=686. الهامش 32 والفراغ 16.

الوصفة 4: خط الاتجاه مع الهدف (Line chart) X = Calendar[Date] (Type: Categorical)، Y = [Attendance Rate %] و [Target 95%] (مقياس ثابت = 0.95).
Lines ← للسلسلة Attendance Rate %: عرض 2، Markers on، لون Phosphate. للسلسلة Target 95%: عرض 1، Dashed، بدون markers، لون Copper.
Y axis: Range من 0.7 إلى 1 (وتكتبين في العنوان الفرعي إن المحور يبدأ من 70%). Data labels on للسلسلة الأولى فقط (Series ← Target off). Legend فوق.
Filters on this visual: Records أكبر من 0 (عشان الأيام الفاضية ما تطلع).
الوصفة 5: أعمدة المناطق بلون شرطي (Clustered bar) Y axis = Employees[AreaClean]، X axis = [Attendance Rate %]. Sort ← by Attendance Rate % تنازليًا.
Bars ← Colors ← fx ← Format style = Gradient، Based on = Attendance Rate %، Minimum = Custom 0.85 لون Copper، Maximum = Custom 0.95 لون Phosphate.
X axis off، Range 0 إلى 1.18 (عشان تطلع الأرقام برا العمود)، Data labels on حجم 8.
Filters: InMaster = Yes، EmploymentStatus = Active أو Vacation.
الوصفة 6: الخريطة الحرارية (Matrix) Rows = Employees[AreaClean] (سمّيه Area)، Columns = Calendar[Date] بصيغة dd MMM، Values = [Attendance Rate %].
أضيفي مقياس اللون: Heat Colour = VAR r = [Attendance Rate %] RETURN SWITCH ( TRUE (), ISBLANK ( r ), "#FFFFFF", r >= 0.95, "#B8A567", r >= 0.85, "#D9CFA3", "#F1EACB" )
Cell elements ← Series = Attendance Rate % ← Background color on ← fx ← Format style = Field value ← What field = Heat Colour.
Style presets = None، Row padding 2، Values وColumn headers محاذاة Center، Subtotals off.
الوصفة 7: جدول Needs attention Columns: EmployeeName (Employee)، [Att Absent] (Absent)، [Att Sick leave] (Sick)، [Missed Days] (No record)، [Attention Score] (Score). Sort by Score تنازليًا.
Filters: EmploymentStatus = Active، Craft ليس Fire Watch / Fabricator / Welder-CS، Attention Score أكبر من 0.
Missed Days = IF ( SELECTEDVALUE ( Employees[EmploymentStatus] ) = "Active", MAX ( 0, [Days In View All] - [Records] ), 0 ) حيث Days In View All يحسب أيام الفلتر بعد إزالة فلتر الموظف (REMOVEFILTERS).

الخطوة 6

الصفحة الواحدة، عنصرًا عنصرًا

الترتيب من فوق لتحت. المواقع بالبكسل: Format ← General ← Properties. الحقول: اسحبيها من قائمة البيانات. الفلاتر: Filters on this visual.

الحضور · Attendance

مين موجود، مين ناقص، وكيف شكل الأسبوع.

#النوعالعنوانالحقولX, YW × Hفلتر العنصر
1CardTitle Text218, 20760 × 34
2CardSub Headline908, 30340 × 24
3SlicerDate96, 66186 × 30
4SlicerAreaClean340, 66150 × 30
5SlicerCraft548, 66176 × 30
6SlicerCategoryClean808, 66150 × 30
7SlicerContractType1038, 66108 × 30
8Text box«ATTENDANCE RATE»44, 116169 × 24
9Text box«ABSENTEEISM, UNPLANNED»249, 116169 × 24
10Text box«PLANNED LEAVE»454, 116169 × 24
11Text box«ON SITE»659, 116169 × 24
12Text box«RECORDS COVERAGE»864, 116169 × 24
13Text box«ACTIVE HEADCOUNT»1069, 116169 × 24
14CardAttendance Rate %39, 138175 × 32
15CardAbsenteeism %244, 138175 × 32
16CardPlanned Leave %449, 138175 × 32
17CardOn Site654, 138175 × 32
18CardCoverage %859, 138175 × 32
19CardActive Headcount1064, 138175 × 32
20CardSub Attendance39, 170175 × 22
21CardSub Absenteeism244, 170175 × 22
22CardSub Planned449, 170175 × 22
23CardSub On Site654, 170175 × 22
24CardSub Coverage v3859, 170175 × 22
25CardSub Headcount1064, 170175 × 22
26lineChartAttendance rate by day, % of scheduled daysDate Attendance Rate % Target 95%32, 214800 × 224[Records] > 0
27Clustered barAttendance rate by area, highest firstAreaClean Attendance Rate %848, 214400 × 224InMaster in Yes · EmploymentStatus in Active, Vacation
28MatrixAttendance rate, area by dayAreaClean Date Attendance Rate %32, 454456 × 222InMaster in Yes · EmploymentStatus in Active, Vacation
29Clustered columnBy weekdayDayName Attendance Rate %504, 454304 × 222[Records] > 0
30TableNeeds attentionEmployeeName Att Absent Att Sick leave Missed Days Attention Score824, 454424 × 222EmploymentStatus in Active · Craft not in Fire Watch, Fabricator, Welder-CS · [Attention Score] > 0
31Text boxتذييل: «Attendance | scheduled days = recorded days minus rest days and exits | absenteeism counts unplanned absence only (absent, sick)»32, 686560 × 30
32Text boxتذييل: «Attendance rate = on site ÷ scheduled days. Coverage = records ÷ (active × days). Sources: attendance macro file on OneDrive + employee master»588, 686660 × 30
تحقق لما تخلصين (بالبيانات الحالية، 1 إلى 5 سبتمبر) العنوان: "Attendance 01 Sep to 05 Sep 2026: 93.4% of scheduled days on site, target 95%".
البطاقات: Attendance rate 93.4%، Absenteeism 0.3%، Planned leave 6.3%، On site 537، Records coverage 70.4%، Active headcount 168.
الخط: 93.3% · 91.2% · 89.6% · 100% · 100% تحت خط الهدف المتقطع. الأعمدة: Train C 100% أعلى، Cooling Tower 93.0% أدنى. الجدول: Narciso Jr. Gallego Dellosa أول اسم (Score 8).

مقاييس جاهزة لو احتجتي صفحة ثانية لاحقًا: IDs Expired، IDs Due 60، On Vacation، New In Contractor List، Att IDs Not In Master. كلها موجودة في _Measures، اسحبيها على بطاقة وخلاص.

الخطوة 7

النشر على SharePoint والتحديث اليومي

  1. بدّلي المسارات إلى روابط (الخطوة 1)
    في Transform data، خطوة Source في Employees وAttendance وContractor. بعدها Close & Apply وتأكدي إن الأرقام نفسها.
  2. Home ← Publish ← Workspace القسم
    لو ما عندك Workspace، IT يسوي واحد باسم PAP Mechanical Maintenance ويعطيك Contributor.
  3. في Power BI Service: Semantic model ← Settings ← Data source credentials
    لكل مصدر: Edit credentials ← OAuth2 ← حسابك في معادن. لا يحتاج Gateway لأن الملفات على SharePoint/OneDrive.
  4. Scheduled refresh: يوميًا 09:00 و 13:00
    الإدخال ينتهي 8 صباحًا، فالساعة 9 يكون داي اليوم ونايت أمس مسجلين.
  5. صفحة SharePoint ← Edit ← + ← Power BI web part ← الصقي رابط التقرير
    هذا اللي تفتحه الإدارة. الصلاحية على التقرير تُعطى من Power BI (Share ← People in your organization with the link) أو كـ App.
ملف الماكرو على OneDrivePower BI يقرأ xlsm عادي (البيانات فقط). لكن لو المسجل فتح الملف وتركه مفتوحًا بدون حفظ، التحديث يقرأ آخر نسخة محفوظة. القاعدة: Save بعد كل إدخال، والملف يبقى بنفس الاسم والمكان.

بعد النشر

الروتين اليومي والفخاخ

متىمينوش يسوي
7:00 إلى 8:00مسؤول الحضوريسجل داي اليوم ويؤكد نايت أمس في ملف الماكرو، ويحفظ
9:00Power BIيتحدث لحاله. الإدارة تفتح صفحة SharePoint
9:15أنتي"Not recorded" و"Active but no record" ← رسالة للمسؤول
كل أحدأنتياحفظي MasterSheet الجديد من السويدي فوق القديم ← Refresh ← أضيفي الجدد للماستر
كل شهرأنتيمن ملف الماستر: الهويات اللي تنتهي خلال 60 يوم ← رسالة للمشرف
الفخوش يصيرالحل
رقم في الحضور مكتوب غلط (44323 بدل C-44323)سجله ما ينربط بموظف ويختفي من الرسومصححيه في ملف الحضور، أو أضيفي الرقم للماستر
موظف بدون رقم معادنلا ينربط حضوره ويطلع في "Active but no record"خذي رقمه من المقاول أو من الباج
فلتر التاريخ فاضيCoverage تقسم على كل الأيام المسجلةهذا صحيح للأسبوع؛ لليوم اختاري يومًا واحدًا
Fire Watch / Fabricator / Welder بدون حضورما يظهرون في "Active but no record" (مستثنون بالفلتر)لو تغير الاستثناء عدلي فلتر الجدول
تغيير اسم الملف أو الجدولRefresh يفشللا تغيرين الأسماء بعد النشر أبدًا

آخر تحديث 15 سبتمبر 2026 · المصدر: 02_Attendance\5_Scripts\att_workforce_pbip.py