قبل ما تبدين
الصورة الكاملة
الملف الجاهز: 02_Attendance\2_Dashboard\PAP_Workforce_Dashboard.pbip (افتحيه في Power BI Desktop، اضغطي Refresh). المولّد 5_Scripts\att_workforce_pbip.py يعيد بناءه بأمر واحد لو تغير شي.
وش تجاوب الصفحة (من فوق لتحت)
| الجزء | السؤال | العنصر |
|---|---|---|
| 6 بطاقات | كم موجود، كم نسبة التغطية، كم غائب ومريض، كم بإجازة أو أوف، كم ما انسجل، وكم الهيدكاونت؟ | On site · Coverage · Absent and sick · On leave or off · Not recorded · Headcount |
| الصف الأول | وصلنا الهدف؟ وش توزيع الحالات؟ والحضور يتحسن أو يسوء يوم بيوم؟ | Gauge · Donut · Combo (أعمدة داي/نايت + خط التغطية) |
| الصف الثاني | مين وين على أي شفت؟ ومين نشط وما له سجل؟ | Bar بالمناطق · جدول Active but no record |
الخطوة 1
ربط الملفات الثلاثة: كيف يتحدث واحد ويظهر في الثاني
ما فيه ملف "يقرأ" ملفًا آخر. الثلاثة مستقلون، وPower BI هو اللي يربطهم وقت التحديث. الربط بمفتاح واحد: الرقم.
| الملف | وين يعيش | الجدول اللي يقرأه Power BI | المفتاح | مين يكتب فيه |
|---|---|---|---|---|
| Daily Attendance.xlsm | OneDrive (مشارك مع مسجلي الحضور، يفتحونه بالديسكتوب عشان الماكرو) | AttendanceTracker | EmployeeID = رقم السويدي للمقاول، أو رقم الموظف للمباشر | مسؤول الحضور |
| PAP_Mech_Employee_Master.xlsx | SharePoint القسم | tblEmployees | MaadenID (المفتاح الرسمي) و SIS_ID | أنتي |
| MasterSheet.xlsx (السويدي) | SharePoint، تحفظينه فوق القديم بنفس الاسم كل ما يوصلك | Table7 | SIS FUSION NUMBER | المقاول (أنتي تحفظينه بس) |
مفتاح الربط: LinkID
ملف الحضور يكتب رقم السويدي للمقاولين ورقم معادن للمباشرين. فبدل ما نغير ملف الحضور، نصنع في الماستر عمودًا محسوبًا في Power Query:
LinkID = if [SIS_ID] <> null and Text.Trim([SIS_ID]) <> "" then Text.Trim([SIS_ID]) else Text.Trim([MaadenID])
وبعدها العلاقة: Attendance[EmployeeID] ← Employees[LinkID]، وContractor[SIS] ← Employees[LinkID]. الاثنان يوصلون لنفس الموظف بدون ما نغير أي ملف مصدر. ولما تحوّلين الحضور لرقم معادن لاحقًا، LinkID يصير MaadenID بس، والعلاقة نفسها.
لما ينضاف موظف جديد
- يوصلك MasterSheet جديد من السويدي: احفظيه فوق القديم بنفس الاسم.
- Refresh: صفحة Contractor & quality تطلع لك "New on contractor list": اسمه ورقمه وتاريخ التعبئة ورقم معادن.
- أضيفيه سطرًا في الماستر (رقم معادن، SIS، الكرافت، المنطقة، الشفت الافتراضي). Refresh مرة ثانية: يختفي من القائمة ويظهر في Headcount، وحضوره يبدأ ينربط من أول يوم يسجله المسؤول.
لما تتغير حالة موظف
لو المقاول كتب Vacation وأنتي عندك Active (أو العكس)، يطلع في "Status mismatch". تصححين الماستر، وتتفقين مع المقاول على المصدر الأصح. والإجازة تظهر في Today كـ Vacation في ملف الحضور، وفي Workforce كـ On vacation من الماستر.
الروابط بدل المسارات (عشان ينشر ويتحدث لحاله)
الملف الجاهز يقرأ من مسارات جهازك. في الدوام غيري سطر Source في كل استعلام (Transform data ← الاستعلام ← الخطوة الأولى) إلى رابط الملف:
// ملف على SharePoint أو OneDrive for Business: خذي الرابط من Excel: File ← Info ← Copy path، واحذفي ?web=1 Source = Excel.Workbook(Web.Contents("https://maaden.sharepoint.com/sites/PAP-Mech/Shared%20Documents/PAP_Mech_Employee_Master.xlsx"), null, true), // نفس الشي لملف الحضور على OneDrive (ملف xlsm يُقرأ عادي، الماكرو ما يهم Power BI) Source = Excel.Workbook(Web.Contents("https://maaden-my.sharepoint.com/personal/alzahraniraw_maaden_com_sa/Documents/Attendance/Daily%20Attendance.xlsm"), null, true),
الخطوة 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
A9Attendance · سجل الحضور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
OutContractor · قائمة المقاول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
TypedCalendar · التقويم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:
IF ( CALCULATE ( COUNTROWS ( Contractor ) ) > 0, "Yes", IF ( Employees[ContractType] = "Direct", "n/a (direct)", "No" ) )
CALCULATE ( MAX ( Contractor[Status] ) )
VAR c = Employees[ContractorStatus] RETURN IF ( ISBLANK ( c ), "n/a", IF ( c = Employees[EmploymentStatus], "Match", "Mismatch: ours " & Employees[EmploymentStatus] & " / contractor " & c ) )
IF ( Employees[EmploymentStatus] = "Vacation", CALCULATE ( MAX ( Contractor[DMOB date] ) ) )
SWITCH ( TRUE (), Employees[InContractorList] = "No", "Not on contractor list", LEFT ( Employees[StatusMatch], 8 ) = "Mismatch", Employees[StatusMatch], "OK" )
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 · الحضور: عدّ الصفوف حسب Presence16 مقياس
COUNTROWS ( Attendance ) + 0
SUM ( Attendance[OnSite] ) + 0
CALCULATE ( [On Site], Attendance[Shift] = "Day" )
CALCULATE ( [On Site], Attendance[Shift] = "Night" )
CALCULATE ( [Records], Attendance[Presence] = "Absent" )
CALCULATE ( [Records], Attendance[Presence] = "Sick leave" )
CALCULATE ( [Records], Attendance[Presence] = "Vacation" )
CALCULATE ( [Records], Attendance[Presence] = "Day off" )
CALCULATE ( [Records], Attendance[Presence] = "Permission" )
CALCULATE ( [Records], Attendance[Presence] = "Exit" )
DISTINCTCOUNT ( Attendance[EmployeeID] ) + 0
CALCULATE ( DISTINCTCOUNT ( Attendance[Date] ) ) + 0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] = "Active" ) + 0
[Active Headcount] * [Days In View]
MAX ( 0, [Expected Records] - [Records] )
[On Site]
02 Ratios · النسب: القسمة الآمنة بـ DIVIDE9 مقياس
DIVIDE ( [Records], [Expected Records] )
DIVIDE ( [On Site], [Records] )
DIVIDE ( [Att Absent] + [Att Sick leave], [Records] )
DIVIDE ( [Att Vacation] + [Att Day off], [Records] )
DIVIDE ( [Night Shift], [On Site] )
DIVIDE ( [On Site], [Days In View] )
MAX ( Attendance[Date] )
1
1
03 Workforce · القوى العاملة: من الماستر10 مقياس
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] IN { "Active", "Vacation" } ) + 0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] = "Vacation" ) + 0
CALCULATE ( COUNTROWS ( Employees ), Employees[EmploymentStatus] IN { "Exit", "Resigned", "Demobilized" } ) + 0
CALCULATE ( [Headcount], Employees[ContractType] = "Direct" )
CALCULATE ( [Headcount], Employees[ContractType] = "Subcontract" )
DIVIDE ( CALCULATE ( [Headcount], Employees[Type of ID] IN { "Permanent", "Permanent (Old ID)", "Maaden Staff" } ), [Headcount] )
CALCULATE ( [Headcount], Employees[Type of ID] = "TAM" )
AVERAGE ( Employees[TenureYears] )
CALCULATE ( [Headcount], Employees[DefaultShift] = "Night" )
CALCULATE ( COUNTROWS ( Employees ), Employees[HasMaadenID] = "Missing" ) + 0
04 IDs & leave · الهويات والإجازات5 مقياس
CALCULATE ( [Headcount], Employees[IDStatus] = "Expired" )
CALCULATE ( [Headcount], Employees[IDStatus] = "Due in 60 days" )
CALCULATE ( [Headcount], Employees[IDStatus] = "Due in 90 days" )
CALCULATE ( [Headcount], Employees[IDStatus] = "No date" )
COUNTROWS ( Employees ) + 0
05 Contractor & quality · المقاول وجودة البيانات6 مقياس
COUNTROWS ( Contractor ) + 0
CALCULATE ( COUNTROWS ( Contractor ), Contractor[InMaster] = "New: not in our master" ) + 0
CALCULATE ( COUNTROWS ( Employees ), Employees[InContractorList] = "No" ) + 0
CALCULATE ( COUNTROWS ( Employees ), LEFT ( Employees[StatusMatch], 8 ) = "Mismatch", Employees[EmploymentStatus] IN { "Active", "Vacation" } ) + 0
CALCULATE ( DISTINCTCOUNT ( Attendance[EmployeeID] ), Attendance[InMaster] = "Not in master" ) + 0
CALCULATE ( COUNTROWS ( Employees ), Employees[AreaClean] = "(No area)" || Employees[CategoryClean] = "(No category)" ) + 0
06 Labels · النصوص الصغيرة تحت الأرقام17 مقياس
"Last day recorded " & FORMAT ( [Last Recorded], "dd MMM yyyy" ) & " · " & FORMAT ( [Active Headcount], "#,0" ) & " active employees"
"Day " & FORMAT ( [Day Shift], "#,0" ) & " · night " & FORMAT ( [Night Shift], "#,0" )
"Recorded " & FORMAT ( [Records], "#,0" ) & " of " & FORMAT ( [Expected Records], "#,0" ) & " expected"
"Absent " & FORMAT ( [Att Absent], "#,0" ) & " · sick " & FORMAT ( [Att Sick leave], "#,0" )
"Vacation " & FORMAT ( [Att Vacation], "#,0" ) & " · off " & FORMAT ( [Att Day off], "#,0" )
"Across " & FORMAT ( [Days In View], "#,0" ) & " day(s) in the filter"
"Distinct people seen " & FORMAT ( [People Seen], "#,0" )
"Direct " & FORMAT ( [Direct Staff], "#,0" ) & " · subcontract " & FORMAT ( [Subcontract Staff], "#,0" )
"Exited or moved " & FORMAT ( [Exited], "#,0" )
"TAM " & FORMAT ( [TAM Count], "#,0" ) & " · night default " & FORMAT ( [Night Default], "#,0" )
"Years since mobilisation, average"
"No expiry date on file: " & FORMAT ( [IDs No Date], "#,0" )
"Cannot be linked to attendance"
"On Al Suwaidi list, missing from our master"
"Subcontract in our master, not on their list"
"Our status vs contractor status"
"Numbers typed in attendance with no employee"
• 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 | الحدود والخطوط الفاصلة |
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) Text box: العنوان "Attendance: who is here, who is missing" حجم 14 عريض Phosphate، x238 y27.
6) Card بالمقياس [Headline] حجم 7.5 رمادي، x762 y36 w470 (يكتب آخر يوم مسجل وعدد النشطين).
لا أزرار تنقل: صفحة واحدة.
Format للـ slicer: Header off، Values حجم 8، خلفية أبيض.
1) مستطيل أبيض 187×84.
2) مستطيل رفيع فوقه 187×3 بلون البطاقة (Phosphate / Gold / Crimson / Copper / 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 خمس مرات، وغيري المقياسين واللون بس.
الرسوم: محور X حجم 9 رمادي، عنوان المحاور off، خطوط الشبكة فاتحة #EFEAE0، Data labels حجم 8 حيث مذكور.
الجداول: رأس الأعمدة خلفية Bone نص Iron حجم 8، القيم حجم 8 مع صفوف متبادلة أبيض/#FBF9F4، خطوط أفقية فقط، Totals off.
الصفوف الثلاثة: البطاقات من y=122، الصف الأول من y=218 بارتفاع 212 (Gauge 236 عرض، Donut 276، والكومبو الباقي)، الصف الثاني من y=442 بارتفاع 232 (Bar 460 عرض، والجدول الباقي)، والتذييل عند y=684 (نص صغير: اليسار عريض Iron، اليمين Aluminum).
الخطوة 6
الصفحة الواحدة، عنصرًا عنصرًا
الترتيب من فوق لتحت. المواقع بالبكسل: Format ← General ← Properties. الحقول: اسحبيها من قائمة البيانات. الفلاتر: Filters on this visual.
الحضور · Attendance
مين موجود، مين ناقص، وكيف شكل الأسبوع.
| # | النوع | العنوان | الحقول | X, Y | W × H | فلتر العنصر |
|---|---|---|---|---|---|---|
| 1 | Card | Headline | 762, 36 | 470 × 26 | — | |
| 2 | Slicer | Date | 112, 80 | 186 × 30 | — | |
| 3 | Slicer | AreaClean | 356, 80 | 150 × 30 | — | |
| 4 | Slicer | Craft | 564, 80 | 176 × 30 | — | |
| 5 | Slicer | CategoryClean | 824, 80 | 150 × 30 | — | |
| 6 | Slicer | ContractType | 1052, 80 | 108 × 30 | — | |
| 7 | Text box | «ON SITE» | — | 60, 128 | 167 × 24 | — |
| 8 | Text box | «COVERAGE OF ACTIVE STAFF» | — | 259, 128 | 167 × 24 | — |
| 9 | Text box | «ABSENT AND SICK» | — | 458, 128 | 167 × 24 | — |
| 10 | Text box | «ON LEAVE OR OFF» | — | 657, 128 | 167 × 24 | — |
| 11 | Text box | «NOT RECORDED» | — | 856, 128 | 167 × 24 | — |
| 12 | Text box | «HEADCOUNT» | — | 1055, 128 | 167 × 24 | — |
| 13 | Card | On Site | 55, 150 | 173 × 32 | — | |
| 14 | Card | Coverage % | 254, 150 | 173 × 32 | — | |
| 15 | Card | Absence % | 453, 150 | 173 × 32 | — | |
| 16 | Card | Leave % | 652, 150 | 173 × 32 | — | |
| 17 | Card | Not Recorded | 851, 150 | 173 × 32 | — | |
| 18 | Card | Headcount | 1050, 150 | 173 × 32 | — | |
| 19 | Card | Sub On Site | 55, 182 | 173 × 22 | — | |
| 20 | Card | Sub Coverage | 254, 182 | 173 × 22 | — | |
| 21 | Card | Sub Absent | 453, 182 | 173 × 22 | — | |
| 22 | Card | Sub Leave | 652, 182 | 173 × 22 | — | |
| 23 | Card | Sub People | 851, 182 | 173 × 22 | — | |
| 24 | Card | Sub Headcount | 1050, 182 | 173 × 22 | — | |
| 25 | gauge | Coverage of active staff | Coverage % Target 100 Max 100 | 48, 218 | 236 × 212 | — |
| 26 | donutChart | Presence mix | Presence Records | 296, 218 | 276 × 212 | — |
| 27 | Line & clustered column | On site by day | Date Day Shift Night Shift Coverage % | 584, 218 | 648 × 212 | [Records] > 0 |
| 28 | Clustered bar | On site by area, day and night | AreaClean Day Shift Night Shift | 48, 442 | 460 × 232 | InMaster in Yes · EmploymentStatus in Active, Vacation |
| 29 | Table | Active but no record in the filter | EmployeeName MaadenID AreaClean Craft DefaultShift Records | 520, 442 | 712 × 232 | EmploymentStatus in Active · Craft not in Fire Watch, Fabricator, Welder-CS · [Records] = 0 |
| 30 | Text box | تذييل: «Attendance | On Duty resolves to the employee's default shift (Day or Night) from the master» | — | 48, 684 | 560 × 30 | — |
| 31 | Text box | تذييل: «On site = Day or Night. Coverage = records ÷ (active employees × days). Sources: attendance macro file on OneDrive + employee master» | — | 572, 684 | 660 × 30 | — |
الدونات: On Site أكبر شريحة. الكومبو: 5 أعمدة (1 إلى 5 سبتمبر) وخط التغطية فوقها. الجدول "Active but no record" فيه 4 أسماء.
مقاييس جاهزة لو احتجتي صفحة ثانية لاحقًا: IDs Expired، IDs Due 60، On Vacation، New In Contractor List، Att IDs Not In Master. كلها موجودة في _Measures، اسحبيها على بطاقة وخلاص.
الخطوة 7
النشر على SharePoint والتحديث اليومي
- بدّلي المسارات إلى روابط (الخطوة 1)في Transform data، خطوة Source في Employees وAttendance وContractor. بعدها Close & Apply وتأكدي إن الأرقام نفسها.
- Home ← Publish ← Workspace القسملو ما عندك Workspace، IT يسوي واحد باسم PAP Mechanical Maintenance ويعطيك Contributor.
- في Power BI Service: Semantic model ← Settings ← Data source credentialsلكل مصدر: Edit credentials ← OAuth2 ← حسابك في معادن. لا يحتاج Gateway لأن الملفات على SharePoint/OneDrive.
- Scheduled refresh: يوميًا 09:00 و 13:00الإدخال ينتهي 8 صباحًا، فالساعة 9 يكون داي اليوم ونايت أمس مسجلين.
- صفحة SharePoint ← Edit ← + ← Power BI web part ← الصقي رابط التقريرهذا اللي تفتحه الإدارة. الصلاحية على التقرير تُعطى من Power BI (Share ← People in your organization with the link) أو كـ App.
بعد النشر
الروتين اليومي والفخاخ
| متى | مين | وش يسوي |
|---|---|---|
| 7:00 إلى 8:00 | مسؤول الحضور | يسجل داي اليوم ويؤكد نايت أمس في ملف الماكرو، ويحفظ |
| 9:00 | Power 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